diff --git a/.gitignore b/.gitignore index f35ca9e..918bf19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle build/ +node_modules/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/README.md b/README.md index 80ea3a6..8e94b11 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ Futur4J is a powerful and intuitive open-source Java library that simplifies asynchronous task scheduling, inspired by the concept of JavaScript promises. -**This documentation is outdated. Please don't read it.** - ## Dependency -The Futur4J project is composed of multiple modules. It is required to include the `futur-api` module, and the other modules depend on it at runtime, however the others are optional and dependent on your use case. +The Futur4J project has a `futur-api` module that provides the core functionality, and a `futur-lazy` module that provides additional static versions of factory methods. It is +recommended to use the main module for customization of logging and execution. ### Gradle ```gradle repositories { @@ -15,8 +14,8 @@ repositories { } dependencies { - compile 'dev.tommyjs:futur-api:2.1.3' - compile 'dev.tommyjs:futur-reactor:2.1.3' + compile 'dev.tommyjs:futur-api:2.4.0' + compile 'dev.tommyjs:futur-lazy:2.4.0' } ``` ### Gradle DSL @@ -26,8 +25,8 @@ repositories { } dependencies { - implementation("dev.tommyjs:futur-api:2.1.3") - implementation("dev.tommyjs:futur-reactor:2.1.3") + implementation("dev.tommyjs:futur-api:2.4.0") + implementation("dev.tommyjs:futur-lazy:2.4.0") } ``` ### Maven @@ -43,121 +42,12 @@ dependencies { dev.tommyjs futur-api - 2.1.3 + 2.4.0 dev.tommyjs - futur-reactor - 2.1.3 + futur-lazy + 2.4.0 -``` - - -## Getting started -Futur4J uses an underlying `Scheduler` instance to power both synchronous and asynchronous task execution. - -The library was originally built for use in Minecraft servers, and therefore has native support for a "main" thread with can be seamlessly switched to in a `Promise` chain (read more below). - -It is recommended, but not required, to manually create a `Scheduler`. If this is not done, a single-threaded default `Scheduler` will be used. -```java -// check if scheduler has already been loaded -if (!Schedulers.isLoaded()) { - Scheduler scheduler = ThreadPoolScheduler.create(6); // create a scheduler using an underlying thread pool (6 threads) - Schedulers.setScheduler(scheduler); // initialize the scheduling -} -``` - -The `futur-standalone` module has two scheduler implementations available. The `ExclusiveThreadPoolScheduler` operates one thread pool, and throws an exception when sync tasks are attempted to be executed. -The `ThreadPoolScheduler` uses a single-threaded "main" pool and a multi-threaded async pool. - -For Minecraft, Bukkit, Velocity and BungeeCord support is coming soon. Feel free to make PRs for other external library support as modules. - -### Using the scheduler -You can invoke tasks using the synchronous and asynchronous scheduler `Scheduler` methods. It is worth noting that exceptions in these tasks are swallowed silently, and it is therefore better to -use promise chains, or use the `Schedulers` utility class, which will wrap tasks in a try-catch and log to the standard error. - -```java -scheduler.runDelayedSync(() -> { - System.out.println("I was scheduled to execute 10 seconds later!"); -}, 10L, TimeUnit.SECONDS); - -Schedulers.runAsync(() -> { - throw new RuntimeException("I will be caught by the exception wrapper in the Schedulers class!"); -}); -``` - -### Reacting to task completions -You may have recieved some form of "future" instance (a task that will complete at an unspecified time in the future). This may be a future4j-native `Promise` instance, or another form of future. In the case of the latter, you may obtain a `Promise` - instance through the various available wrappers (e.g. `ReactorTransformer` for use with reactor core). - -You can then add listeners to the `Promise`, which will be executed asynchronously on promise "completion". A promise is deemed completed once the task has either concluded successfully or an exception has been thrown. You can access this information in a `PromiseCompletion`, -which is passed to promise listeners. Promise listeners should not throw exceptions, and will print a stack trace if they do to avoid silent failure. However, callbacks in promise chain methods are permitted to throw exceptions as they will be passed down the chain. -```java -Promise promise = doSomeAsyncProcessing(); -promise.addListener(ctx -> { - if (ctx.isError()) { - System.out.println("Error! Oh no!"); - } else { - String result = ctx.getResult(); - System.out.println(result); - } -}); -``` - -### Promise chains -`Promise` also contains convenience wrapper methods to avoid checking the completion for exceptions every time. These methods also return a new promise which will resolve once the original promise, and then handler callback have finished execution. We can use this feature to create -"promise chains", which are a sequence of promise handlers. These handlers are permitted to throw exceptions, which will be passed down promise chains until they are handled. If exceptions are not handled properly, they will be silently swallowed. It is recommend to append `logExceptions()` -to chains unless exceptions are handled explicitly with a custom listener. This will simply log exceptions in the promise chain to the standard error. - -You can also use the sync and async methods to choose which executor is chosen for task execution. The ability to seamlessly switch between executors comes in handy with applications where you must be on the "main thread" to perform non-threadsafe operations. - -`Promise.start()` can be used to create a "resolved" promise. This is useful when starting a promise chain or returning an already-completed promise (common in compose callbacks, mentioned below). - -```java -Promise.start().thenSupplyAsync(() -> { - // do some async processing - return "Hello World!"; -}).thenApplyAsync(input -> { - // convert string to upper case - return input.toUpperCase(); -}).thenConsumeSync(result -> { - // display result - System.out.println("Result: " + result); -}).logExceptions(); // log exceptions to the standard error -``` - -The promise chain methods follow Java convention of `Supplier`, `Consumer` and `Runnable` through the `thenSupply`, `thenConsume` and `thenRun` methods. There is also `thenApply` (which acts as a Java `Function`), and `thenCompose`, which is explained below. Futur4J has its own implementations of the Java concepts though to allow for -exceptions within handlers. - -The `thenCompose` method is similar to the concept in the Java `CompletableFuture` API. You are able to return another `Promise` within a promise handler, and a `Promise>` will be wrapped up into just a `Promise`. This is often useful when using an external library that returns some sort of "future" inside a handler. - -```java -String username = "abc123"; -mySuperSecretApi.usernameToId(username) // returns Promise - .thenComposeAsync(id -> { - return userManager.fetchFromDatabase(id); // returns Promise - }).thenConsumeSync(playerData -> { - System.out.println(username + " has " + playerData.getCoins() + " coins!"); - }).logExceptions(); -``` - -### Utility methods -The `Promises` utility class provides many helpful methods for working with promises and groups of promises. - -`Promises.combine(Promise, Promise)` combines two promises into one `Promise>`. - -`Promises.erase(Promise)` erases the type on a `Promise` instance and returns a `Promise`. This is also supported for lists of promises with `Promises.all(List)`, which will return a `Promise` representing the future whereby all promises have completed. - -If all promises are of identical type, you can use `Promises.combine(List>)` which will return one `Promise>`, similarly representing the future whereby all promises have completed. - -This can also be applied to key-value scenarios in the same way, where you can provide a mapper function to be applied to all elements and combine into a single promise. - - - - - -### Future wrappers -External libaries provide asynchronous ways to interact with their output in all shapes and sizes. Futur4J currently has wrappers for the following libraries/frameworks: -- Reactor Core (via futur-reactor) -- Java CompletableFuture (via `Promises.wrap` in futur-api) +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index efb73d0..052bbc3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' + id 'java-library' id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' } nexusPublishing { @@ -14,9 +14,9 @@ nexusPublishing { subprojects { group = 'dev.tommyjs' - version = '2.3.4' + version = '2.4.0' - apply plugin: 'java' + apply plugin: 'java-library' apply plugin: 'com.github.johnrengelman.shadow' tasks { @@ -30,8 +30,9 @@ subprojects { } dependencies { - implementation 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:24.1.0' implementation 'org.slf4j:slf4j-api:2.0.12' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'io.projectreactor:reactor-core:3.6.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' @@ -42,6 +43,13 @@ subprojects { useJUnitPlatform() testLogging { exceptionFormat = 'full' + showStandardStreams = true } } + + java { + sourceCompatibility = JavaVersion.VERSION_22 + targetCompatibility = JavaVersion.VERSION_22 + withSourcesJar() + } } \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/executor/DualPoolExecutor.java b/futur-api/src/main/java/dev/tommyjs/futur/executor/DualPoolExecutor.java deleted file mode 100644 index df0d998..0000000 --- a/futur-api/src/main/java/dev/tommyjs/futur/executor/DualPoolExecutor.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.tommyjs.futur.executor; - -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class DualPoolExecutor implements PromiseExecutor> { - - private final @NotNull ScheduledExecutorService syncSvc; - private final @NotNull ScheduledExecutorService asyncSvc; - - public DualPoolExecutor(@NotNull ScheduledExecutorService syncSvc, @NotNull ScheduledExecutorService asyncSvc) { - this.syncSvc = syncSvc; - this.asyncSvc = asyncSvc; - } - - public static @NotNull DualPoolExecutor create(int asyncPoolSize) { - return new DualPoolExecutor(Executors.newSingleThreadScheduledExecutor(), Executors.newScheduledThreadPool(asyncPoolSize)); - } - - @Override - public Future runSync(@NotNull Runnable task, long delay, @NotNull TimeUnit unit) { - return syncSvc.schedule(task, delay, unit); - } - - @Override - public Future runAsync(@NotNull Runnable task, long delay, @NotNull TimeUnit unit) { - return asyncSvc.schedule(task, delay, unit); - } - - @Override - public void cancel(Future task) { - task.cancel(true); - } - -} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/executor/ExecutorServiceImpl.java b/futur-api/src/main/java/dev/tommyjs/futur/executor/ExecutorServiceImpl.java new file mode 100644 index 0000000..acefe38 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/executor/ExecutorServiceImpl.java @@ -0,0 +1,32 @@ +package dev.tommyjs.futur.executor; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +class ExecutorServiceImpl implements PromiseExecutor> { + + private final ScheduledExecutorService service; + + public ExecutorServiceImpl(@NotNull ScheduledExecutorService service) { + this.service = service; + } + + @Override + public Future run(@NotNull Runnable task) { + return service.submit(task); + } + + @Override + public Future run(@NotNull Runnable task, long delay, @NotNull TimeUnit unit) { + return service.schedule(task, delay, unit); + } + + @Override + public boolean cancel(Future task) { + return task.cancel(true); + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/executor/PromiseExecutor.java b/futur-api/src/main/java/dev/tommyjs/futur/executor/PromiseExecutor.java index caeaad8..1143ec2 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/executor/PromiseExecutor.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/executor/PromiseExecutor.java @@ -2,22 +2,80 @@ package dev.tommyjs.futur.executor; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +/** + * An executor that can run tasks and schedule tasks to run in the future. + */ public interface PromiseExecutor { - T runSync(@NotNull Runnable task, long delay, @NotNull TimeUnit unit); - - T runAsync(@NotNull Runnable task, long delay, @NotNull TimeUnit unit); - - default T runSync(@NotNull Runnable task) { - return runSync(task, 0L, TimeUnit.MILLISECONDS); + /** + * Creates a new {@link PromiseExecutor} that runs tasks on virtual threads. + * + * @return the new executor + */ + static PromiseExecutor virtualThreaded() { + return new VirtualThreadImpl(); } - default T runAsync(@NotNull Runnable task) { - return runAsync(task, 0L, TimeUnit.MILLISECONDS); + /** + * Creates a new {@link PromiseExecutor} that runs tasks on a single thread. + * + * @return the new executor + */ + static PromiseExecutor singleThreaded() { + return of(Executors.newSingleThreadScheduledExecutor()); } - void cancel(T task); + /** + * Creates a new {@link PromiseExecutor} that runs tasks on multiple threads. + * + * @param threads the number of threads + * @return the new executor + */ + static PromiseExecutor multiThreaded(int threads) { + return of(Executors.newScheduledThreadPool(threads)); + } + + /** + * Creates a new {@link PromiseExecutor} that runs tasks on the given executor service. + * + * @param service the executor service + * @return the new executor + */ + static PromiseExecutor of(@NotNull ScheduledExecutorService service) { + return new ExecutorServiceImpl(service); + } + + /** + * Runs the given task. + * + * @param task the task + * @return the task + * @throws Exception if scheduling the task failed + */ + T run(@NotNull Runnable task) throws Exception; + + /** + * Runs the given task after the given delay. + * + * @param task the task + * @param delay the delay + * @param unit the time unit + * @return the task + * @throws Exception if scheduling the task failed + */ + T run(@NotNull Runnable task, long delay, @NotNull TimeUnit unit) throws Exception; + + /** + * Cancels the given task if possible. This may interrupt the task mid-execution. + * + * @param task the task + * @return {@code true} if the task was cancelled. {@code false} if the task was already completed + * or could not be cancelled. + */ + boolean cancel(T task); } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/executor/SinglePoolExecutor.java b/futur-api/src/main/java/dev/tommyjs/futur/executor/SinglePoolExecutor.java deleted file mode 100644 index c63dab6..0000000 --- a/futur-api/src/main/java/dev/tommyjs/futur/executor/SinglePoolExecutor.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.tommyjs.futur.executor; - -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -public class SinglePoolExecutor extends DualPoolExecutor { - - public SinglePoolExecutor(@NotNull ScheduledExecutorService service) { - super(service, service); - } - - public static @NotNull SinglePoolExecutor create(int threadPoolSize) { - return new SinglePoolExecutor(Executors.newScheduledThreadPool(threadPoolSize)); - } - -} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/executor/VirtualThreadImpl.java b/futur-api/src/main/java/dev/tommyjs/futur/executor/VirtualThreadImpl.java new file mode 100644 index 0000000..397efc3 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/executor/VirtualThreadImpl.java @@ -0,0 +1,36 @@ +package dev.tommyjs.futur.executor; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; + +class VirtualThreadImpl implements PromiseExecutor { + + @Override + public Thread run(@NotNull Runnable task) { + return Thread.ofVirtual().start(task); + } + + @Override + public Thread run(@NotNull Runnable task, long delay, @NotNull TimeUnit unit) { + return Thread.ofVirtual().start(() -> { + try { + Thread.sleep(unit.toMillis(delay)); + } catch (InterruptedException e) { + return; + } + task.run(); + }); + } + + @Override + public boolean cancel(Thread task) { + if (task.isAlive()) { + task.interrupt(); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalConsumer.java b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalConsumer.java index 3998e57..8d96919 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalConsumer.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalConsumer.java @@ -1,8 +1,20 @@ package dev.tommyjs.futur.function; +/** + * Represents an operation that accepts a single input argument and returns no result, + * and may throw an exception. This is a functional interface whose functional method is {@link #accept(Object)}. + * + * @param the type of the input to the operation + */ @FunctionalInterface public interface ExceptionalConsumer { - void accept(T value) throws Throwable; + /** + * Performs this operation on the given argument, potentially throwing an exception. + * + * @param value the input argument + * @throws Exception if unable to compute a result + */ + void accept(T value) throws Exception; -} +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalFunction.java b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalFunction.java index ebebf5f..403da2b 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalFunction.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalFunction.java @@ -1,8 +1,22 @@ package dev.tommyjs.futur.function; +/** + * Represents a function that accepts one argument and produces a result, + * and may throw an exception. This is a functional interface whose functional method is {@link #apply(Object)}. + * + * @param the type of the input to the function + * @param the type of the result of the function + */ @FunctionalInterface public interface ExceptionalFunction { - V apply(K value) throws Throwable; + /** + * Applies this function to the given argument, potentially throwing an exception. + * + * @param value the input argument + * @return the function result + * @throws Exception if unable to compute a result + */ + V apply(K value) throws Exception; -} +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalRunnable.java b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalRunnable.java index c4b8002..fc130cb 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalRunnable.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalRunnable.java @@ -1,8 +1,17 @@ package dev.tommyjs.futur.function; +/** + * Represents a runnable task that may throw an exception. + * This is a functional interface whose functional method is {@link #run()}. + */ @FunctionalInterface public interface ExceptionalRunnable { - void run() throws Throwable; + /** + * Performs this runnable task, potentially throwing an exception. + * + * @throws Exception if unable to complete the task + */ + void run() throws Exception; -} +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalSupplier.java b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalSupplier.java index e47f977..ba39049 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalSupplier.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/function/ExceptionalSupplier.java @@ -1,8 +1,20 @@ package dev.tommyjs.futur.function; +/** + * Represents a supplier of results that may throw an exception. + * This is a functional interface whose functional method is {@link #get()}. + * + * @param the type of results supplied by this supplier + */ @FunctionalInterface public interface ExceptionalSupplier { - T get() throws Throwable; + /** + * Gets a result, potentially throwing an exception. + * + * @return a result + * @throws Exception if unable to supply a result + */ + T get() throws Exception; -} +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromise.java b/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromise.java deleted file mode 100644 index 8427c8a..0000000 --- a/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromise.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.tommyjs.futur.impl; - -import dev.tommyjs.futur.executor.PromiseExecutor; -import dev.tommyjs.futur.promise.AbstractPromise; -import dev.tommyjs.futur.promise.AbstractPromiseFactory; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; - -public class SimplePromise extends AbstractPromise { - - private final @NotNull AbstractPromiseFactory factory; - - public SimplePromise(@NotNull AbstractPromiseFactory factory) { - this.factory = factory; - } - - @Deprecated - public SimplePromise(@NotNull PromiseExecutor executor, @NotNull Logger logger, @NotNull AbstractPromiseFactory factory) { - this(factory); - } - - @Override - public @NotNull AbstractPromiseFactory getFactory() { - return factory; - } - -} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromiseFactory.java b/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromiseFactory.java deleted file mode 100644 index 1bc6ecb..0000000 --- a/futur-api/src/main/java/dev/tommyjs/futur/impl/SimplePromiseFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.tommyjs.futur.impl; - -import dev.tommyjs.futur.executor.PromiseExecutor; -import dev.tommyjs.futur.promise.AbstractPromiseFactory; -import dev.tommyjs.futur.promise.Promise; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; - -public class SimplePromiseFactory extends AbstractPromiseFactory { - - private final PromiseExecutor executor; - private final Logger logger; - - public SimplePromiseFactory(PromiseExecutor executor, Logger logger) { - this.executor = executor; - this.logger = logger; - } - - @Override - public @NotNull Promise unresolved() { - return new SimplePromise<>(this); - } - - @Override - public @NotNull Logger getLogger() { - return logger; - } - - @Override - public @NotNull PromiseExecutor getExecutor() { - return executor; - } - -} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/joiner/CompletionJoiner.java b/futur-api/src/main/java/dev/tommyjs/futur/joiner/CompletionJoiner.java new file mode 100644 index 0000000..82f9539 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/joiner/CompletionJoiner.java @@ -0,0 +1,47 @@ +package dev.tommyjs.futur.joiner; + +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import dev.tommyjs.futur.util.ConcurrentResultArray; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.List; + +public class CompletionJoiner extends PromiseJoiner, Void, Void, List>> { + + private final ConcurrentResultArray> results; + + public CompletionJoiner( + @NotNull PromiseFactory factory, + @NotNull Iterator> promises, + int expectedSize + ) { + super(factory); + results = new ConcurrentResultArray<>(expectedSize); + join(promises); + } + + @Override + protected Void getChildKey(Promise value) { + return null; + } + + @Override + protected @NotNull Promise getChildPromise(Promise value) { + //noinspection unchecked + return (Promise) value; + } + + @Override + protected void onChildComplete(int index, Void key, @NotNull PromiseCompletion res) { + results.set(index, res); + } + + @Override + protected List> getResult() { + return results.toList(); + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/joiner/MappedResultJoiner.java b/futur-api/src/main/java/dev/tommyjs/futur/joiner/MappedResultJoiner.java new file mode 100644 index 0000000..ee64b0e --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/joiner/MappedResultJoiner.java @@ -0,0 +1,51 @@ +package dev.tommyjs.futur.joiner; + +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import dev.tommyjs.futur.util.ConcurrentResultArray; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class MappedResultJoiner extends PromiseJoiner>, K, V, Map> { + + private final @NotNull ConcurrentResultArray> results; + + public MappedResultJoiner( + @NotNull PromiseFactory factory, + @NotNull Iterator>> promises, + int expectedSize + ) { + super(factory); + this.results = new ConcurrentResultArray<>(expectedSize); + join(promises); + } + + @Override + protected K getChildKey(Map.Entry> entry) { + return entry.getKey(); + } + + @Override + protected @NotNull Promise getChildPromise(Map.Entry> entry) { + return entry.getValue(); + } + + @Override + protected void onChildComplete(int index, K key, @NotNull PromiseCompletion res) { + results.set(index, new AbstractMap.SimpleImmutableEntry<>(key, res.getResult())); + } + + @Override + protected Map getResult() { + List> list = results.toList(); + Map map = new HashMap<>(list.size()); + for (Map.Entry entry : list) { + map.put(entry.getKey(), entry.getValue()); + } + + return map; + } + +} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/joiner/PromiseJoiner.java b/futur-api/src/main/java/dev/tommyjs/futur/joiner/PromiseJoiner.java new file mode 100644 index 0000000..56b465f --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/joiner/PromiseJoiner.java @@ -0,0 +1,69 @@ +package dev.tommyjs.futur.joiner; + +import dev.tommyjs.futur.promise.CompletablePromise; +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import dev.tommyjs.futur.util.PromiseUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class PromiseJoiner { + + private final CompletablePromise joined; + + protected PromiseJoiner(@NotNull PromiseFactory factory) { + this.joined = factory.unresolved(); + } + + protected abstract Key getChildKey(T value); + + protected abstract @NotNull Promise getChildPromise(T value); + + protected abstract void onChildComplete(int index, Key key, @NotNull PromiseCompletion completion); + + protected abstract Result getResult(); + + protected void join(@NotNull Iterator promises) { + AtomicInteger count = new AtomicInteger(); + + int i = 0; + do { + if (joined.isCompleted()) { + promises.forEachRemaining(v -> getChildPromise(v).cancel()); + return; + } + + T value = promises.next(); + Promise p = getChildPromise(value); + if (!p.isCompleted()) { + PromiseUtil.cancelOnComplete(joined, p); + } + + count.incrementAndGet(); + Key key = getChildKey(value); + int index = i++; + + p.addAsyncListener(res -> { + onChildComplete(index, key, res); + if (res.isError()) { + assert res.getException() != null; + joined.completeExceptionally(res.getException()); + } else if (count.decrementAndGet() == -1) { + joined.complete(getResult()); + } + }); + } while (promises.hasNext()); + + if (count.decrementAndGet() == -1) { + joined.complete(getResult()); + } + } + + public @NotNull Promise joined() { + return joined; + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/joiner/ResultJoiner.java b/futur-api/src/main/java/dev/tommyjs/futur/joiner/ResultJoiner.java new file mode 100644 index 0000000..b4e1a2d --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/joiner/ResultJoiner.java @@ -0,0 +1,46 @@ +package dev.tommyjs.futur.joiner; + +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import dev.tommyjs.futur.util.ConcurrentResultArray; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.List; + +public class ResultJoiner extends PromiseJoiner, Void, T, List> { + + private final ConcurrentResultArray results; + + public ResultJoiner( + @NotNull PromiseFactory factory, + @NotNull Iterator> promises, + int expectedSize + ) { + super(factory); + this.results = new ConcurrentResultArray<>(expectedSize); + join(promises); + } + + @Override + protected Void getChildKey(Promise value) { + return null; + } + + @Override + protected @NotNull Promise getChildPromise(Promise value) { + return value; + } + + @Override + protected void onChildComplete(int index, Void key, @NotNull PromiseCompletion res) { + results.set(index, res.getResult()); + } + + @Override + protected List getResult() { + return results.toList(); + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/joiner/VoidJoiner.java b/futur-api/src/main/java/dev/tommyjs/futur/joiner/VoidJoiner.java new file mode 100644 index 0000000..56a58d5 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/joiner/VoidJoiner.java @@ -0,0 +1,38 @@ +package dev.tommyjs.futur.joiner; + +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +public class VoidJoiner extends PromiseJoiner, Void, Void, Void> { + + public VoidJoiner(@NotNull PromiseFactory factory, @NotNull Iterator> promises) { + super(factory); + join(promises); + } + + @Override + protected Void getChildKey(Promise value) { + return null; + } + + @Override + protected @NotNull Promise getChildPromise(Promise value) { + //noinspection unchecked + return (Promise) value; + } + + @Override + protected void onChildComplete(int index, Void key, @NotNull PromiseCompletion completion) { + + } + + @Override + protected Void getResult() { + return null; + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromise.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromise.java index bb823eb..149df3c 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromise.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromise.java @@ -1,111 +1,133 @@ package dev.tommyjs.futur.promise; -import dev.tommyjs.futur.executor.PromiseExecutor; import dev.tommyjs.futur.function.ExceptionalConsumer; import dev.tommyjs.futur.function.ExceptionalFunction; import dev.tommyjs.futur.function.ExceptionalRunnable; import dev.tommyjs.futur.function.ExceptionalSupplier; +import dev.tommyjs.futur.util.PromiseUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; -import java.util.Collection; -import java.util.LinkedList; -import java.util.Objects; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; -public abstract class AbstractPromise implements Promise { +public abstract class AbstractPromise implements Promise { - private Collection> listeners; - private final AtomicReference> completion; - private final CountDownLatch latch; - private final Lock lock; + public abstract @NotNull AbstractPromiseFactory getFactory(); - public AbstractPromise() { - this.completion = new AtomicReference<>(); - this.latch = new CountDownLatch(1); - this.lock = new ReentrantLock(); - } - - protected static void propagateResult(Promise from, Promise to) { - from.addDirectListener(to::complete, to::completeExceptionally); - } - - protected static void propagateCancel(Promise from, Promise to) { - from.onCancel(to::completeExceptionally); - } - - private @NotNull Runnable createRunnable(T result, @NotNull Promise promise, @NotNull ExceptionalFunction task) { - return () -> { - if (promise.isCompleted()) return; - - try { - V nextResult = task.apply(result); - promise.complete(nextResult); - } catch (Throwable e) { - promise.completeExceptionally(e); - } - }; - } - - public abstract @NotNull AbstractPromiseFactory getFactory(); - - protected @NotNull PromiseExecutor getExecutor() { - return getFactory().getExecutor(); - } + protected abstract @NotNull Promise addAnyListener(@NotNull PromiseListener listener); protected @NotNull Logger getLogger() { return getFactory().getLogger(); } - @Override - public T awaitInterruptibly() throws InterruptedException { - this.latch.await(); - return joinCompletion(Objects.requireNonNull(getCompletion())); - } - - @Override - public T awaitInterruptibly(long timeoutMillis) throws TimeoutException, InterruptedException { - boolean success = this.latch.await(timeoutMillis, TimeUnit.MILLISECONDS); - if (!success) { - throw new TimeoutException("Promise stopped waiting after " + timeoutMillis + "ms"); + protected void callListener(@NotNull PromiseListener listener, @NotNull PromiseCompletion cmp) { + if (listener instanceof AsyncPromiseListener) { + callListenerAsync(listener, cmp); + } else { + callListenerNow(listener, cmp); } - - return joinCompletion(Objects.requireNonNull(getCompletion())); } - @Override - public T await() { + protected V supplySafe(@NotNull ExceptionalSupplier supplier, @NotNull Function handler) { try { - return awaitInterruptibly(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + return supplier.get(); + } catch (Error error) { + // Rethrow error so the Thread can shut down + throw error; + } catch (Throwable e) { + return handler.apply(e); } } + protected void runSafe(@NotNull ExceptionalRunnable runnable, @NotNull Consumer handler) { + try { + runnable.run(); + } catch (Error error) { + handler.accept(error); + // Rethrow error so the Thread can shut down + throw error; + } catch (Throwable e) { + handler.accept(e); + } + } + + protected void runCompleter(@NotNull CompletablePromise promise, @NotNull ExceptionalRunnable completer) { + runSafe(completer, promise::completeExceptionally); + } + + protected V useCompletion(Supplier unresolved, Function completed, Function failed) { + PromiseCompletion completion = getCompletion(); + if (completion == null) return unresolved.get(); + else if (completion.isSuccess()) return completed.apply(completion.getResult()); + else return failed.apply(completion.getException()); + } + + protected @NotNull Runnable createCompleter(T result, @NotNull CompletablePromise promise, + @NotNull ExceptionalFunction completer) { + return () -> { + if (!promise.isCompleted()) { + runCompleter(promise, () -> promise.complete(completer.apply(result))); + } + }; + } + + protected @NotNull CompletablePromise createLinked() { + CompletablePromise promise = getFactory().unresolved(); + PromiseUtil.propagateCancel(promise, this); + return promise; + } + + protected void callListenerAsync(PromiseListener listener, PromiseCompletion res) { + try { + getFactory().getAsyncExecutor().run(() -> callListenerNow(listener, res)); + } catch (RejectedExecutionException ignored) { + } catch (Exception e) { + getLogger().warn("Exception caught while running promise listener", e); + } + } + + protected void callListenerNow(PromiseListener listener, PromiseCompletion res) { + runSafe(() -> listener.handle(res), e -> getLogger().error("Exception caught in promise listener", e)); + } + + protected void callListenerAsyncLastResort(PromiseListener listener, PromiseCompletion completion) { + try { + getFactory().getAsyncExecutor().run(() -> callListenerNow(listener, completion)); + } catch (Throwable ignored) { + } + } + + protected T joinCompletionChecked() throws ExecutionException { + PromiseCompletion completion = getCompletion(); + assert completion != null; + if (completion.isSuccess()) return completion.getResult(); + throw new ExecutionException(completion.getException()); + } + + protected T joinCompletionUnchecked() { + PromiseCompletion completion = getCompletion(); + assert completion != null; + if (completion.isSuccess()) return completion.getResult(); + throw new CompletionException(completion.getException()); + } + @Override - public T await(long timeoutMillis) throws TimeoutException { - try { - return awaitInterruptibly(timeoutMillis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } + public @NotNull Promise fork() { + if (isCompleted()) return this; - private T joinCompletion(PromiseCompletion completion) { - if (completion.isError()) - throw new RuntimeException(completion.getException()); - - return completion.getResult(); + CompletablePromise fork = getFactory().unresolved(); + PromiseUtil.propagateCompletion(this, fork); + return fork; } @Override public @NotNull Promise thenRun(@NotNull ExceptionalRunnable task) { - return thenApply(result -> { + return thenApply(_ -> { task.run(); return null; }); @@ -121,43 +143,71 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise thenSupply(@NotNull ExceptionalSupplier task) { - return thenApply(result -> task.get()); + return thenApply(_ -> task.get()); } @Override public @NotNull Promise thenApply(@NotNull ExceptionalFunction task) { - Promise promise = getFactory().unresolved(); - addDirectListener( - res -> createRunnable(res, promise, task).run(), - promise::completeExceptionally - ); + return useCompletion( + () -> { + CompletablePromise promise = createLinked(); + addDirectListener( + res -> createCompleter(res, promise, task).run(), + promise::completeExceptionally + ); - propagateCancel(promise, this); - return promise; + return promise; + }, + result -> supplySafe( + () -> getFactory().resolve(task.apply(result)), + getFactory()::error + ), + getFactory()::error + ); } @Override public @NotNull Promise thenCompose(@NotNull ExceptionalFunction> task) { - Promise promise = getFactory().unresolved(); - thenApply(task).addDirectListener( - nestedPromise -> { - if (nestedPromise == null) { - promise.complete(null); - } else { - propagateResult(nestedPromise, promise); - propagateCancel(promise, nestedPromise); - } - }, - promise::completeExceptionally - ); + return useCompletion( + () -> { + CompletablePromise promise = createLinked(); + thenApply(task).addDirectListener( + result -> { + if (result == null) { + promise.complete(null); + } else { + PromiseUtil.propagateCompletion(result, promise); + PromiseUtil.propagateCancel(promise, result); + } + }, + promise::completeExceptionally + ); - propagateCancel(promise, this); - return promise; + return promise; + }, + result -> supplySafe( + () -> { + Promise nested = task.apply(result); + if (nested == null) { + return getFactory().resolve(null); + } else if (nested.isCompleted()) { + return nested; + } else { + CompletablePromise promise = createLinked(); + PromiseUtil.propagateCompletion(nested, promise); + PromiseUtil.propagateCancel(promise, nested); + return promise; + } + }, + getFactory()::error + ), + getFactory()::error + ); } @Override public @NotNull Promise thenRunSync(@NotNull ExceptionalRunnable task) { - return thenApplySync(result -> { + return thenApplySync(_ -> { task.run(); return null; }); @@ -165,7 +215,7 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise thenRunDelayedSync(@NotNull ExceptionalRunnable task, long delay, @NotNull TimeUnit unit) { - return thenApplyDelayedSync(result -> { + return thenApplyDelayedSync(_ -> { task.run(); return null; }, delay, unit); @@ -189,76 +239,65 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise thenSupplySync(@NotNull ExceptionalSupplier task) { - return thenApplySync(result -> task.get()); + return thenApplySync(_ -> task.get()); } @Override public @NotNull Promise thenSupplyDelayedSync(@NotNull ExceptionalSupplier task, long delay, @NotNull TimeUnit unit) { - return thenApplyDelayedSync(result -> task.get(), delay, unit); + return thenApplyDelayedSync(_ -> task.get(), delay, unit); } @Override public @NotNull Promise thenApplySync(@NotNull ExceptionalFunction task) { - Promise promise = getFactory().unresolved(); + CompletablePromise promise = createLinked(); addDirectListener( - res -> { - try { - Runnable runnable = createRunnable(res, promise, task); - F future = getExecutor().runSync(runnable); - promise.onCancel((e) -> getExecutor().cancel(future)); - } catch (RejectedExecutionException e) { - promise.completeExceptionally(e); - } - }, + res -> runCompleter(promise, () -> { + Runnable runnable = createCompleter(res, promise, task); + FS future = getFactory().getSyncExecutor().run(runnable); + promise.addDirectListener(_ -> getFactory().getSyncExecutor().cancel(future)); + }), promise::completeExceptionally ); - propagateCancel(promise, this); return promise; } @Override public @NotNull Promise thenApplyDelayedSync(@NotNull ExceptionalFunction task, long delay, @NotNull TimeUnit unit) { - Promise promise = getFactory().unresolved(); + CompletablePromise promise = createLinked(); addDirectListener( - res -> { - try { - Runnable runnable = createRunnable(res, promise, task); - F future = getExecutor().runSync(runnable, delay, unit); - promise.onCancel((e) -> getExecutor().cancel(future)); - } catch (RejectedExecutionException e) { - promise.completeExceptionally(e); - } - }, + res -> runCompleter(promise, () -> { + Runnable runnable = createCompleter(res, promise, task); + FS future = getFactory().getSyncExecutor().run(runnable, delay, unit); + promise.addDirectListener(_ -> getFactory().getSyncExecutor().cancel(future)); + }), promise::completeExceptionally ); - propagateCancel(promise, this); return promise; } @Override public @NotNull Promise thenComposeSync(@NotNull ExceptionalFunction> task) { - Promise promise = getFactory().unresolved(); + CompletablePromise promise = createLinked(); thenApplySync(task).addDirectListener( nestedPromise -> { if (nestedPromise == null) { promise.complete(null); } else { - propagateResult(nestedPromise, promise); - propagateCancel(promise, nestedPromise); + PromiseUtil.propagateCompletion(nestedPromise, promise); + PromiseUtil.propagateCancel(promise, nestedPromise); } }, promise::completeExceptionally ); - propagateCancel(promise, this); return promise; } @Override public @NotNull Promise thenRunAsync(@NotNull ExceptionalRunnable task) { - return thenApplyAsync(result -> { + return thenApplyAsync(_ -> { task.run(); return null; }); @@ -266,7 +305,7 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise thenRunDelayedAsync(@NotNull ExceptionalRunnable task, long delay, @NotNull TimeUnit unit) { - return thenApplyDelayedAsync(result -> { + return thenApplyDelayedAsync(_ -> { task.run(); return null; }, delay, unit); @@ -290,84 +329,73 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise thenSupplyAsync(@NotNull ExceptionalSupplier task) { - return thenApplyAsync(result -> task.get()); + return thenApplyAsync(_ -> task.get()); } @Override public @NotNull Promise thenSupplyDelayedAsync(@NotNull ExceptionalSupplier task, long delay, @NotNull TimeUnit unit) { - return thenApplyDelayedAsync(result -> task.get(), delay, unit); + return thenApplyDelayedAsync(_ -> task.get(), delay, unit); + } + + @Override + public @NotNull Promise thenApplyAsync(@NotNull ExceptionalFunction task) { + CompletablePromise promise = createLinked(); + addDirectListener( + (res) -> runCompleter(promise, () -> { + Runnable runnable = createCompleter(res, promise, task); + FA future = getFactory().getAsyncExecutor().run(runnable); + promise.addDirectListener(_ -> getFactory().getAsyncExecutor().cancel(future)); + }), + promise::completeExceptionally + ); + + return promise; + } + + @Override + public @NotNull Promise thenApplyDelayedAsync(@NotNull ExceptionalFunction task, long delay, @NotNull TimeUnit unit) { + CompletablePromise promise = createLinked(); + addDirectListener( + res -> runCompleter(promise, () -> { + Runnable runnable = createCompleter(res, promise, task); + FA future = getFactory().getAsyncExecutor().run(runnable, delay, unit); + promise.addDirectListener(_ -> getFactory().getAsyncExecutor().cancel(future)); + }), + promise::completeExceptionally + ); + + return promise; + } + + @Override + public @NotNull Promise thenComposeAsync(@NotNull ExceptionalFunction> task) { + CompletablePromise promise = createLinked(); + thenApplyAsync(task).addDirectListener( + nestedPromise -> { + if (nestedPromise == null) { + promise.complete(null); + } else { + PromiseUtil.propagateCompletion(nestedPromise, promise); + PromiseUtil.propagateCancel(promise, nestedPromise); + } + }, + promise::completeExceptionally + ); + + return promise; } @Override public @NotNull Promise thenPopulateReference(@NotNull AtomicReference reference) { - return thenApplyAsync((result) -> { + return thenApply(result -> { reference.set(result); return result; }); } - @Override - public @NotNull Promise thenApplyAsync(@NotNull ExceptionalFunction task) { - Promise promise = getFactory().unresolved(); - addDirectListener( - (res) -> { - try { - Runnable runnable = createRunnable(res, promise, task); - F future = getExecutor().runAsync(runnable); - promise.onCancel((e) -> getExecutor().cancel(future)); - } catch (RejectedExecutionException e) { - promise.completeExceptionally(e); - } - }, - promise::completeExceptionally - ); - - propagateCancel(promise, this); - return promise; - } - - @Override - public @NotNull Promise thenApplyDelayedAsync(@NotNull ExceptionalFunction task, long delay, @NotNull TimeUnit unit) { - Promise promise = getFactory().unresolved(); - addDirectListener( - res -> { - try { - Runnable runnable = createRunnable(res, promise, task); - F future = getExecutor().runAsync(runnable, delay, unit); - promise.onCancel((e) -> getExecutor().cancel(future)); - } catch (RejectedExecutionException e) { - promise.completeExceptionally(e); - } - }, - promise::completeExceptionally - ); - - propagateCancel(promise, this); - return promise; - } - - @Override - public @NotNull Promise thenComposeAsync(@NotNull ExceptionalFunction> task) { - Promise promise = getFactory().unresolved(); - thenApplyAsync(task).addDirectListener( - nestedPromise -> { - if (nestedPromise == null) { - promise.complete(null); - } else { - propagateResult(nestedPromise, promise); - propagateCancel(promise, nestedPromise); - } - }, - promise::completeExceptionally - ); - - propagateCancel(promise, this); - return promise; - } - @Override public @NotNull Promise erase() { - return thenSupplyAsync(() -> null); + return thenSupply(() -> null); } @Override @@ -377,11 +405,11 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise addAsyncListener(@Nullable Consumer successListener, @Nullable Consumer errorListener) { - return addAsyncListener((res) -> { - if (res.isError()) { - if (errorListener != null) errorListener.accept(res.getException()); - } else { + return addAsyncListener(res -> { + if (res.isSuccess()) { if (successListener != null) successListener.accept(res.getResult()); + } else { + if (errorListener != null) errorListener.accept(res.getException()); } }); } @@ -393,54 +421,15 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise addDirectListener(@Nullable Consumer successListener, @Nullable Consumer errorListener) { - return addDirectListener((res) -> { - if (res.isError()) { - if (errorListener != null) errorListener.accept(res.getException()); - } else { + return addDirectListener(res -> { + if (res.isSuccess()) { if (successListener != null) successListener.accept(res.getResult()); + } else { + if (errorListener != null) errorListener.accept(res.getException()); } }); } - private @NotNull Promise addAnyListener(PromiseListener listener) { - PromiseCompletion completion; - - lock.lock(); - try { - completion = getCompletion(); - if (completion == null) { - if (listeners == null) listeners = new LinkedList<>(); - listeners.add(listener); - return this; - } - } finally { - lock.unlock(); - } - - callListener(listener, completion); - return this; - } - - private void callListener(PromiseListener listener, PromiseCompletion ctx) { - if (listener instanceof AsyncPromiseListener) { - try { - getExecutor().runAsync(() -> callListenerNow(listener, ctx)); - } catch (RejectedExecutionException ignored) { - - } - } else { - callListenerNow(listener, ctx); - } - } - - private void callListenerNow(PromiseListener listener, PromiseCompletion ctx) { - try { - listener.handle(ctx); - } catch (Exception e) { - getLogger().error("Exception caught in promise listener", e); - } - } - @Override public @NotNull Promise onSuccess(@NotNull Consumer listener) { return addAsyncListener(listener, null); @@ -453,13 +442,14 @@ public abstract class AbstractPromise implements Promise { @Override public @NotNull Promise logExceptions(@NotNull String message) { - return onError(e -> getLogger().error(message, e)); + Exception wrapper = new DeferredExecutionException(); + return onError(e -> getLogger().error(message, wrapper.initCause(e))); } @Override - public @NotNull Promise onError(@NotNull Class clazz, @NotNull Consumer listener) { - return onError((e) -> { - if (clazz.isAssignableFrom(e.getClass())) { + public @NotNull Promise onError(@NotNull Class type, @NotNull Consumer listener) { + return onError(e -> { + if (type.isAssignableFrom(e.getClass())) { //noinspection unchecked listener.accept((E) e); } @@ -471,79 +461,50 @@ public abstract class AbstractPromise implements Promise { return onError(CancellationException.class, listener); } - @Deprecated @Override - public @NotNull Promise timeout(long time, @NotNull TimeUnit unit) { - return maxWaitTime(time, unit); + public @NotNull Promise orDefault(@Nullable T defaultValue) { + return orDefault(_ -> defaultValue); } @Override - public @NotNull Promise maxWaitTime(long time, @NotNull TimeUnit unit) { - try { - Exception e = new TimeoutException("Promise stopped waiting after " + time + " " + unit); - F future = getExecutor().runAsync(() -> completeExceptionally(e), time, unit); - return addDirectListener((_v) -> getExecutor().cancel(future)); - } catch (RejectedExecutionException e) { - completeExceptionally(e); - return this; - } - } - - private void handleCompletion(@NotNull PromiseCompletion ctx) { - lock.lock(); - try { - if (!setCompletion(ctx)) return; - - this.latch.countDown(); - if (listeners != null) { - for (PromiseListener listener : listeners) { - callListener(listener, ctx); - } - } - } finally { - lock.unlock(); - } - } - - private boolean setCompletion(PromiseCompletion completion) { - return this.completion.compareAndSet(null, completion); + public @NotNull Promise orDefault(@NotNull ExceptionalSupplier supplier) { + return orDefault(_ -> supplier.get()); } @Override - public void cancel(@Nullable String message) { - completeExceptionally(new CancellationException(message)); - } - - @Override - public void complete(@Nullable T result) { - handleCompletion(new PromiseCompletion<>(result)); - } - - @Override - public void completeExceptionally(@NotNull Throwable result) { - handleCompletion(new PromiseCompletion<>(result)); - } - - @Override - public boolean isCompleted() { - return completion.get() != null; - } - - @Override - public @Nullable PromiseCompletion getCompletion() { - return completion.get(); + public @NotNull Promise orDefault(@NotNull ExceptionalFunction function) { + return useCompletion( + () -> { + CompletablePromise promise = createLinked(); + addDirectListener(promise::complete, e -> runCompleter(promise, () -> promise.complete(function.apply(e)))); + return promise; + }, + getFactory()::resolve, + getFactory()::error + ); } @Override public @NotNull CompletableFuture toFuture() { - CompletableFuture future = new CompletableFuture<>(); - this.addDirectListener(future::complete, future::completeExceptionally); - future.whenComplete((res, e) -> { - if (e instanceof CancellationException) { - this.cancel(); - } - }); - return future; + return useCompletion( + () -> { + CompletableFuture future = new CompletableFuture<>(); + addDirectListener(future::complete, future::completeExceptionally); + future.whenComplete((_, e) -> { + if (e instanceof CancellationException) { + cancel(); + } + }); + + return future; + }, + CompletableFuture::completedFuture, + CompletableFuture::failedFuture + ); + } + + private static class DeferredExecutionException extends ExecutionException { + } } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromiseFactory.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromiseFactory.java index 80a4e52..7ce84d1 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromiseFactory.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/AbstractPromiseFactory.java @@ -1,159 +1,30 @@ package dev.tommyjs.futur.promise; import dev.tommyjs.futur.executor.PromiseExecutor; +import dev.tommyjs.futur.joiner.CompletionJoiner; +import dev.tommyjs.futur.joiner.MappedResultJoiner; +import dev.tommyjs.futur.joiner.ResultJoiner; +import dev.tommyjs.futur.joiner.VoidJoiner; +import dev.tommyjs.futur.util.PromiseUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -public abstract class AbstractPromiseFactory implements PromiseFactory { +public abstract class AbstractPromiseFactory implements PromiseFactory { - public abstract @NotNull PromiseExecutor getExecutor(); + public abstract @NotNull Logger getLogger(); + + public abstract @NotNull PromiseExecutor getSyncExecutor(); + + public abstract @NotNull PromiseExecutor getAsyncExecutor(); @Override - public @NotNull Promise> combine(boolean propagateCancel, @NotNull Promise p1, @NotNull Promise p2) { - List> promises = List.of(p1, p2); - return all(propagateCancel, promises) - .thenApplyAsync((res) -> new AbstractMap.SimpleImmutableEntry<>( - Objects.requireNonNull(p1.getCompletion()).getResult(), - Objects.requireNonNull(p2.getCompletion()).getResult() - )); - } - - @Override - public @NotNull Promise> combine(boolean propagateCancel, @NotNull Map> promises, @Nullable BiConsumer exceptionHandler) { - if (promises.isEmpty()) return resolve(Collections.emptyMap()); - - Map map = new HashMap<>(); - Promise> promise = unresolved(); - for (Map.Entry> entry : promises.entrySet()) { - if (propagateCancel) { - AbstractPromise.propagateCancel(promise, entry.getValue()); - } - - entry.getValue().addDirectListener((ctx) -> { - synchronized (map) { - if (ctx.getException() != null) { - if (exceptionHandler == null) { - promise.completeExceptionally(ctx.getException()); - } else { - exceptionHandler.accept(entry.getKey(), ctx.getException()); - map.put(entry.getKey(), null); - } - } else { - map.put(entry.getKey(), ctx.getResult()); - } - - if (map.size() == promises.size()) { - promise.complete(map); - } - } - }); - } - - return promise; - } - - @Override - public @NotNull Promise> combine(boolean propagateCancel, @NotNull Iterable> promises, @Nullable BiConsumer exceptionHandler) { - AtomicInteger index = new AtomicInteger(); - return this.combine( - propagateCancel, - StreamSupport.stream(promises.spliterator(), false) - .collect(Collectors.toMap(k -> index.getAndIncrement(), v -> v)), - exceptionHandler - ).thenApplyAsync(v -> - v.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(Map.Entry::getValue) - .collect(Collectors.toList()) - ); - } - - @Override - public @NotNull Promise>> allSettled(boolean propagateCancel, @NotNull Iterable> promiseIterable) { - List> promises = new ArrayList<>(); - promiseIterable.iterator().forEachRemaining(promises::add); - - if (promises.isEmpty()) return resolve(Collections.emptyList()); - PromiseCompletion[] results = new PromiseCompletion[promises.size()]; - - Promise>> promise = unresolved(); - var iter = promises.listIterator(); - - while (iter.hasNext()) { - int index = iter.nextIndex(); - var p = iter.next(); - - if (propagateCancel) { - AbstractPromise.propagateCancel(promise, p); - } - - p.addDirectListener((res) -> { - synchronized (results) { - results[index] = res; - if (Arrays.stream(results).allMatch(Objects::nonNull)) - promise.complete(Arrays.asList(results)); - } - }); - } - - return promise; - } - - @Override - public @NotNull Promise all(boolean propagateCancel, @NotNull Iterable> promiseIterable) { - List> promises = new ArrayList<>(); - promiseIterable.iterator().forEachRemaining(promises::add); - - if (promises.isEmpty()) return resolve(null); - AtomicInteger completed = new AtomicInteger(); - Promise promise = unresolved(); - - for (Promise p : promises) { - if (propagateCancel) { - AbstractPromise.propagateCancel(promise, p); - } - - p.addDirectListener((res) -> { - if (res.getException() != null) { - promise.completeExceptionally(res.getException()); - } else if (completed.incrementAndGet() == promises.size()) { - promise.complete(null); - } - }); - } - - return promise; - } - - @Override - public @NotNull Promise race(boolean cancelRaceLosers, @NotNull Iterable> promises) { - Promise promise = unresolved(); - for (Promise p : promises) { - if (cancelRaceLosers) { - promise.addListener((res) -> p.cancel()); - } - AbstractPromise.propagateResult(p, promise); - } - return promise; - } - - @Override - public @NotNull Promise wrap(@NotNull CompletableFuture future) { - return wrap(future, future); - } - - private @NotNull Promise wrap(@NotNull CompletionStage completion, Future future) { - Promise promise = unresolved(); - + public @NotNull Promise wrap(@NotNull CompletionStage completion, @Nullable Future future) { + CompletablePromise promise = unresolved(); completion.whenComplete((v, e) -> { if (e != null) { promise.completeExceptionally(e); @@ -162,21 +33,73 @@ public abstract class AbstractPromiseFactory implements PromiseFactory { } }); - promise.onCancel((e) -> future.cancel(true)); + if (future != null) { + promise.onCancel(_ -> future.cancel(true)); + } + return promise; } @Override - public @NotNull Promise resolve(T value) { - Promise promise = unresolved(); - promise.complete(value); - return promise; + public @NotNull Promise> combine( + @NotNull Promise p1, @NotNull Promise p2 + ) { + return all(p1, p2).thenApply(_ -> new AbstractMap.SimpleImmutableEntry<>( + Objects.requireNonNull(p1.getCompletion()).getResult(), + Objects.requireNonNull(p2.getCompletion()).getResult() + )); } @Override - public @NotNull Promise error(@NotNull Throwable error) { - Promise promise = unresolved(); - promise.completeExceptionally(error); + public @NotNull Promise> combineMapped( + @NotNull Iterator>> promises, + int expectedSize + ) { + if (!promises.hasNext()) return resolve(Collections.emptyMap()); + return new MappedResultJoiner<>(this, promises, expectedSize).joined(); + } + + @Override + public @NotNull Promise> combine( + @NotNull Iterator> promises, + int expectedSize + ) { + if (!promises.hasNext()) return resolve(Collections.emptyList()); + return new ResultJoiner<>(this, promises, expectedSize).joined(); + } + + @Override + public @NotNull Promise>> allSettled( + @NotNull Iterator> promises, + int expectedSize + ) { + if (!promises.hasNext()) return resolve(Collections.emptyList()); + return new CompletionJoiner(this, promises, expectedSize).joined(); + } + + @Override + public @NotNull Promise all(@NotNull Iterator> promises) { + if (!promises.hasNext()) return resolve(null); + return new VoidJoiner(this, promises).joined(); + } + + @Override + public @NotNull Promise race( + @NotNull Iterator> promises, + boolean ignoreErrors + ) { + CompletablePromise promise = unresolved(); + while (promises.hasNext()) { + if (promise.isCompleted()) { + promises.forEachRemaining(Promise::cancel); + break; + } + + Promise p = promises.next(); + PromiseUtil.cancelOnComplete(promise, p); + p.addDirectListener(promise::complete, ignoreErrors ? null : promise::completeExceptionally); + } + return promise; } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/AsyncPromiseListener.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/AsyncPromiseListener.java index 7e0343b..799b6be 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/AsyncPromiseListener.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/AsyncPromiseListener.java @@ -1,5 +1,8 @@ package dev.tommyjs.futur.promise; +/** + * A listener for a {@link Promise} that is called when the promise is resolved. This listener is + * executed asynchronously by the {@link PromiseFactory} that created the completed promise. + */ public interface AsyncPromiseListener extends PromiseListener { - } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/BasePromise.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/BasePromise.java new file mode 100644 index 0000000..45c8c48 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/BasePromise.java @@ -0,0 +1,193 @@ +package dev.tommyjs.futur.promise; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.*; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +@SuppressWarnings({"FieldMayBeFinal"}) +public abstract class BasePromise extends AbstractPromise implements CompletablePromise { + + private static final VarHandle COMPLETION_HANDLE; + private static final VarHandle LISTENERS_HANDLE; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + COMPLETION_HANDLE = lookup.findVarHandle(BasePromise.class, "completion", PromiseCompletion.class); + LISTENERS_HANDLE = lookup.findVarHandle(BasePromise.class, "listeners", Collection.class); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private final Sync sync; + + private volatile PromiseCompletion completion; + + @SuppressWarnings("FieldMayBeFinal") + private volatile Collection> listeners; + + @SuppressWarnings("unchecked") + public BasePromise() { + this.sync = new Sync(); + this.completion = null; + this.listeners = Collections.EMPTY_LIST; + } + + protected void handleCompletion(@NotNull PromiseCompletion cmp) { + if (!COMPLETION_HANDLE.compareAndSet(this, null, cmp)) return; + sync.releaseShared(1); + callListeners(cmp); + } + + protected Promise completeExceptionallyDelayed(Throwable e, long delay, TimeUnit unit) { + runCompleter(this, () -> { + FA future = getFactory().getAsyncExecutor().run(() -> completeExceptionally(e), delay, unit); + addDirectListener(_ -> getFactory().getAsyncExecutor().cancel(future)); + }); + + return this; + } + + @SuppressWarnings("unchecked") + protected void callListeners(@NotNull PromiseCompletion cmp) { + Iterator> iter = ((Iterable>) LISTENERS_HANDLE.getAndSet(this, null)).iterator(); + try { + while (iter.hasNext()) { + callListener(iter.next(), cmp); + } + } finally { + iter.forEachRemaining(v -> callListenerAsyncLastResort(v, cmp)); + } + } + + @Override + protected @NotNull Promise addAnyListener(@NotNull PromiseListener listener) { + Collection> prev = listeners, next = null; + for (boolean haveNext = false; ; ) { + if (!haveNext) { + next = prev == Collections.EMPTY_LIST ? new ConcurrentLinkedQueue<>() : prev; + if (next != null) next.add(listener); + } + + if (LISTENERS_HANDLE.weakCompareAndSet(this, prev, next)) + break; + + haveNext = (prev == (prev = listeners)); + } + + if (next == null) { + callListener(listener, Objects.requireNonNull(getCompletion())); + } + + return this; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (!isCompleted()) { + sync.acquireSharedInterruptibly(1); + } + + return joinCompletionChecked(); + } + + @Override + public T get(long time, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (!isCompleted()) { + boolean success = sync.tryAcquireSharedNanos(1, unit.toNanos(time)); + if (!success) { + throw new TimeoutException("Promise stopped waiting after " + time + " " + unit); + } + } + + return joinCompletionChecked(); + } + + @Override + public T await() { + if (!isCompleted()) { + try { + sync.acquireSharedInterruptibly(1); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + return joinCompletionUnchecked(); + } + + @Override + public @NotNull Promise timeout(long time, @NotNull TimeUnit unit) { + Exception e = new CancellationException("Promise timed out after " + time + " " + unit.toString().toLowerCase()); + return completeExceptionallyDelayed(e, time, unit); + } + + @Override + public @NotNull Promise maxWaitTime(long time, @NotNull TimeUnit unit) { + Exception e = new TimeoutException("Promise stopped waiting after " + time + " " + unit.toString().toLowerCase()); + return completeExceptionallyDelayed(e, time, unit); + } + + @Override + public void cancel(@NotNull CancellationException e) { + completeExceptionally(e); + } + + @Override + public void complete(@Nullable T result) { + handleCompletion(new PromiseCompletion<>(result)); + } + + @Override + public void completeExceptionally(@NotNull Throwable result) { + handleCompletion(new PromiseCompletion<>(result)); + } + + @Override + public boolean isCompleted() { + return completion != null; + } + + @Override + public @Nullable PromiseCompletion getCompletion() { + return completion; + } + + private static final class Sync extends AbstractQueuedSynchronizer { + + private Sync() { + setState(1); + } + + @Override + protected int tryAcquireShared(int acquires) { + return getState() == 0 ? 1 : -1; + } + + @Override + protected boolean tryReleaseShared(int releases) { + int c1, c2; + do { + c1 = getState(); + if (c1 == 0) { + return false; + } + + c2 = c1 - 1; + } while (!compareAndSetState(c1, c2)); + + return c2 == 0; + } + + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletablePromise.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletablePromise.java new file mode 100644 index 0000000..998566a --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletablePromise.java @@ -0,0 +1,25 @@ +package dev.tommyjs.futur.promise; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link Promise} that can be completed. + */ +public interface CompletablePromise extends Promise { + + /** + * Completes the promise successfully with the given result. + * + * @param result the result + */ + void complete(@Nullable T result); + + /** + * Completes the promise exceptionally with the given exception. + * + * @param result the exception + */ + void completeExceptionally(@NotNull Throwable result); + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletedPromise.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletedPromise.java new file mode 100644 index 0000000..d13fa2c --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/CompletedPromise.java @@ -0,0 +1,72 @@ +package dev.tommyjs.futur.promise; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public abstract class CompletedPromise extends AbstractPromise { + + private static final PromiseCompletion EMPTY = new PromiseCompletion<>(); + + private final @NotNull PromiseCompletion completion; + + public CompletedPromise(@NotNull PromiseCompletion completion) { + this.completion = completion; + } + + @SuppressWarnings("unchecked") + public CompletedPromise() { + this((PromiseCompletion) EMPTY); + } + + @Override + protected @NotNull Promise addAnyListener(@NotNull PromiseListener listener) { + callListener(listener, completion); + return this; + } + + @Override + public @NotNull Promise timeout(long time, @NotNull TimeUnit unit) { + // Promise is already completed so can't time out + return this; + } + + @Override + public @NotNull Promise maxWaitTime(long time, @NotNull TimeUnit unit) { + // Promise is already completed so can't time out + return this; + } + + @Override + public void cancel(@NotNull CancellationException exception) { + // Promise is already completed so can't be cancelled + } + + @Override + public T get() throws ExecutionException { + return joinCompletionChecked(); + } + + @Override + public T get(long timeout, @NotNull TimeUnit unit) throws ExecutionException { + return joinCompletionChecked(); + } + + @Override + public T await() { + return joinCompletionUnchecked(); + } + + @Override + public @NotNull PromiseCompletion getCompletion() { + return completion; + } + + @Override + public boolean isCompleted() { + return true; + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/Promise.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/Promise.java index 1fc93f4..f4628ce 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/Promise.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/Promise.java @@ -8,157 +8,603 @@ import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +/** + *

+ * A promise represents the result of an asynchronous computation. A promise will transition from a + * pending state to a completed state at most once, but may remain in a pending state indefinitely. + *

+ * + *

+ * Promises are created by a {@link PromiseFactory} and support chaining operations to be executed + * upon completion. These operations can be synchronous or asynchronous, and can be composed in a + * variety of ways. Promises can be listened to for completions, either with a result or with an + * exception. Promises can be cancelled, which will propagate a cancellation signal through the + * chain, but a promise can also be forked, which will prevent propagation of cancellations. + *

+ * + * @see #cancel() + * @see #fork() + */ public interface Promise { - PromiseFactory getFactory(); + /** + * Returns the factory that created this promise. This factory can be used to create new promises. + */ + @NotNull PromiseFactory getFactory(); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * when this promise completes. Cancelling the returned promise will cancel this promise, and + * consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenRun(@NotNull ExceptionalRunnable task); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * when this promise completes and will be passed the result of this promise. Cancelling the returned + * promise will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenConsume(@NotNull ExceptionalConsumer task); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * when this promise completes, and will supply a value to the next promise in the chain. Cancelling + * the returned promise will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenSupply(@NotNull ExceptionalSupplier task); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * when this promise completes, and will apply the specified function to the result of this promise + * in order to supply a value to the next promise in the chain. Cancelling the returned promise will + * cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenApply(@NotNull ExceptionalFunction task); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * when this promise completes, and will compose the next promise in the chainfrom the result of + * this promise. Cancelling the returned promise will cancel this promise, and consequently any + * previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, once this promise and the promise returned by + * the task are complete, with the result of the task promise + */ @NotNull Promise thenCompose(@NotNull ExceptionalFunction> task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * sync executor of the factory that created this promise, immediately after this promise completes. + * Cancelling the returned promise will cancel this promise, and consequently any previous promises + * in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenRunSync(@NotNull ExceptionalRunnable task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * sync executor of the factory that created this promise, after the specified delay after this + * promise completes. Cancelling the returned promise will cancel this promise, and consequently + * any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenRunDelayedSync(@NotNull ExceptionalRunnable task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * sync executor of the factory that created this promise immediately after this promise completes, + * and will be passed the result of this promise. Cancelling the returned promise will cancel this + * promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenConsumeSync(@NotNull ExceptionalConsumer task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * sync executor of the factory that created this promise after the specified delay after this + * promise completes, and will be passed the result of this promise. Cancelling the returned promise + * will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenConsumeDelayedSync(@NotNull ExceptionalConsumer task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed immediately + * by the sync executor of the factory that created this promise when this promise completes, and + * will supply a value to the next promise in the chain. Cancelling the returned promise will cancel + * this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenSupplySync(@NotNull ExceptionalSupplier task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the sync + * executor of the factory that created this promise after the specified delay after this promise + * completes, and will supply a value to the next promise in the chain. Cancelling the returned promise + * will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenSupplyDelayedSync(@NotNull ExceptionalSupplier task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the sync + * executor of the factory that created this promise immediately after this promise completes, and + * will apply the specified function to the result of this promise in order to supply a value to the + * next promise in the chain. Cancelling the returned promise will cancel this promise, and consequently + * any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenApplySync(@NotNull ExceptionalFunction task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the sync + * executor of the factory that created this promise after the specified delay after this promise + * completes, and will apply the specified function to the result of this promise in order to supply + * a value to the next promise in the chain. Cancelling the returned promise will cancel this promise, + * and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenApplyDelayedSync(@NotNull ExceptionalFunction task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the sync + * executor of the factory that created this promise immediately after this promise completes, and + * will compose the next promise in the chain from the result of this promise. Cancelling the returned + * promise will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, once this promise and the promise returned by the task are + * complete, with the result of the task promise + */ @NotNull Promise thenComposeSync(@NotNull ExceptionalFunction> task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * async executor of the factory that created this promise, immediately after this promise completes. + * Cancelling the returned promise will cancel this promise, and consequently any previous promises + * in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenRunAsync(@NotNull ExceptionalRunnable task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * async executor of the factory that created this promise after the specified delay after this + * promise completes. Cancelling the returned promise will cancel this promise, and consequently + * any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenRunDelayedAsync(@NotNull ExceptionalRunnable task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * async executor of the factory that created this promise immediately after this promise completes, + * and will be passed the result of this promise. Cancelling the returned promise will cancel this + * promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenConsumeAsync(@NotNull ExceptionalConsumer task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * async executor of the factory that created this promise after the specified delay after this + * promise completes, and will be passed the result of this promise. Cancelling the returned promise + * will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes after the task is executed + */ @NotNull Promise thenConsumeDelayedAsync(@NotNull ExceptionalConsumer task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the + * async executor of the factory that created this promise immediately after this promise completes, + * and will supply a value to the next promise in the chain. Cancelling the returned promise will + * cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenSupplyAsync(@NotNull ExceptionalSupplier task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the async + * executor of the factory that created this promise after the specified delay after this promise + * completes, and will supply a value to the next promise in the chain. Cancelling the returned promise + * will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenSupplyDelayedAsync(@NotNull ExceptionalSupplier task, long delay, @NotNull TimeUnit unit); - @NotNull Promise thenPopulateReference(@NotNull AtomicReference reference); - + /** + * Chains a task to be executed after this promise completes. The task will be executed by the async + * executor of the factory that created this promise immediately after this promise completes, and + * will apply the specified function to the result of this promise in order to supply a value to the + * next promise in the chain. Cancelling the returned promise will cancel this promise, and consequently + * any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenApplyAsync(@NotNull ExceptionalFunction task); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the async + * executor of the factory that created this promise after the specified delay after this promise + * completes, and will apply the specified function to the result of this promise in order to supply + * a value to the next promise in the chain. Cancelling the returned promise will cancel this promise, + * and consequently any previous promises in the chain. + * + * @param task the task to execute + * @param delay the amount of time to wait before executing the task + * @param unit the time unit of the delay + * @return a new promise that completes, after the task is executed, with the task result + */ @NotNull Promise thenApplyDelayedAsync(@NotNull ExceptionalFunction task, long delay, @NotNull TimeUnit unit); + /** + * Chains a task to be executed after this promise completes. The task will be executed by the async + * executor of the factory that created this promise immediately after this promise completes, and + * will compose the next promise in the chain from the result of this promise. Cancelling the returned + * promise will cancel this promise, and consequently any previous promises in the chain. + * + * @param task the task to execute + * @return a new promise that completes, once this promise and the promise returned by the task are + * complete, with the result of the task promise + */ @NotNull Promise thenComposeAsync(@NotNull ExceptionalFunction> task); + /** + * Adds a listener to this promise that will populate the specified reference with the result of this + * promise upon successful completion. The reference will not be populated if this promise completes + * exceptionally. + * + * @param reference the reference to populate + * @return continuation of the promise chain + */ + @NotNull Promise thenPopulateReference(@NotNull AtomicReference reference); + + /** + * Returns a promise backed by this promise that will complete with {@code null} if this promise + * completes successfully, or with the exception if this promise completes exceptionally. Cancelling + * the returned promise will cancel this promise, and consequently any previous promises in the chain. + */ @NotNull Promise erase(); + /** + * Logs any exceptions that occur in the promise chain with the specified message. The stack trace + * will be captured immediately when invoking this method, and logged alongside an exception if + * encountered, to allow for easier debugging. + * + * @param message the message to log + * @return continuation of the promise chain + */ + @NotNull Promise logExceptions(@NotNull String message); + + /** + * Logs any exceptions that occur in the promise chain. The stack trace will be captured immediately + * when invoking this method, and logged alongside an exception if encountered, to allow for easier + * debugging. + * + * @return continuation of the promise chain + */ default @NotNull Promise logExceptions() { return logExceptions("Exception caught in promise chain"); } - @NotNull Promise logExceptions(@NotNull String message); - /** - * @apiNote Direct listeners run on the same thread as the completion. + * Adds a listener to this promise that will be executed immediately when this promise completes, + * on the same thread as the completion call. + * + * @param listener the listener to add + * @return continuation of the promise chain */ @NotNull Promise addDirectListener(@NotNull PromiseListener listener); + /** + * Adds a listener to this promise that will be executed immediately when this promise completes, + * on the same thread as the completion call. One of {@code successHandler} and {@code errorHandler} will be + * called when the promise completes successfully or exceptionally, respectively. + * + * @param successHandler the function to call on success + * @param errorHandler the function to call on error + * @return continuation of the promise chain + */ @NotNull Promise addDirectListener(@Nullable Consumer successHandler, @Nullable Consumer errorHandler); /** - * @apiNote Async listeners are run in parallel. + * Adds a listener to this promise that will be executed immediately when this promise completes, + * by the async executor of the factory that created this promise. + * + * @param listener the listener to add + * @return continuation of the promise chain */ @NotNull Promise addAsyncListener(@NotNull AsyncPromiseListener listener); /** - * @apiNote Same as addAsyncListener. + * Adds a listener to this promise that will be executed immediately when this promise completes. + * + * @param listener the listener to add + * @return continuation of the promise chain */ default @NotNull Promise addListener(@NotNull AsyncPromiseListener listener) { return addAsyncListener(listener); } + /** + * Adds a listener to this promise that will be executed immediately when this promise completes, + * by the async executor of the factory that created this promise. One of {@code successHandler} and + * {@code errorHandler} will be called when the promise completes successfully or exceptionally, respectively. + * + * @param successHandler the function to call on success + * @param errorHandler the function to call on error + */ @NotNull Promise addAsyncListener(@Nullable Consumer successHandler, @Nullable Consumer errorHandler); + /** + * Adds a listener to this promise that will be called if the promise is completed successfully. + * + * @param listener the listener to add + * @return continuation of the promise chain + */ @NotNull Promise onSuccess(@NotNull Consumer listener); + /** + * Adds a listener to this promise that will be called if the promise is completed exceptionally. + * + * @param listener the listener to add + * @return continuation of the promise chain + */ @NotNull Promise onError(@NotNull Consumer listener); - @NotNull Promise onError(@NotNull Class clazz, @NotNull Consumer listener); + /** + * Adds a listener to this promise that will be called if the promise is completed exceptionally + * with an exception of the specified type. + * + * @param listener the listener to add + * @param type the class of the exception to listen for + * @return continuation of the promise chain + */ + @NotNull Promise onError(@NotNull Class type, @NotNull Consumer listener); + /** + * Adds a listener to this promise that will be called if the promise is cancelled. + * + * @param listener the listener to add + * @return continuation of the promise chain + */ @NotNull Promise onCancel(@NotNull Consumer listener); /** - * @deprecated Use maxWaitTime instead + * Creates a new promise that will always complete successfully - either with the result of this + * promise, or with the specified default value if this promise completes exceptionally. Cancelling + * the returned promise will cancel this promise, and consequently any previous promises in the chain. + * + * @param defaultValue the default value to complete the promise with if this promise completes exceptionally + * @return a new promise that completes with the result of this promise, or with the default value if this + * promise completes exceptionally + */ + @NotNull Promise orDefault(@Nullable T defaultValue); + + /** + * Creates a new promise that will attempt to always complete successfully - either with the result + * of this promise, or with the result of the specified supplier if this promise completes exceptionally. + * If an exception is encountered while executing the supplier, the promise will complete exceptionally + * with that exception. Cancelling the returned promise will cancel this promise, and consequently any + * previous promises in the chain. + * + * @param supplier the supplier to complete the promise with if this promise completes exceptionally + * @return a new promise that completes with the result of this promise, or with the result of the + * supplier if this promise completes exceptionally + */ + @NotNull Promise orDefault(@NotNull ExceptionalSupplier supplier); + + /** + * Creates a new promise that will attempt to always complete successfully - either with the result + * of this promise, or with the result of the specified function if this promise completes + * exceptionally. If an exception is encountered while executing the function, the promise will + * complete exceptionally with that exception. Cancelling the returned promise will cancel this + * promise, and consequently any previous promises in the chain. + * + * @param function the function to complete the promise with if this promise completes exceptionally + * @return a new promise that completes with the result of this promise, or with the result of the + * function if this promise completes exceptionally + */ + @NotNull Promise orDefault(@NotNull ExceptionalFunction function); + + /** + * Cancels the promise if not already completed after the specified timeout. This will result in + * an exceptional completion with a {@link CancellationException}. + * + * @param time the amount of time to wait before cancelling the promise + * @param unit the time unit of the delay + * @return continuation of the promise chain */ - @Deprecated @NotNull Promise timeout(long time, @NotNull TimeUnit unit); /** - * @deprecated Use maxWaitTime instead + * Cancels the promise if not already completed after the specified timeout. This will result in + * an exceptional completion with a {@link CancellationException}. + * + * @param ms the amount of time to wait before cancelling the promise (in milliseconds) + * @return continuation of the promise chain */ - @Deprecated default @NotNull Promise timeout(long ms) { return timeout(ms, TimeUnit.MILLISECONDS); } + /** + * Times out the promise if not already completed after the specified timeout. This will result + * in an exceptional completion with a {@link TimeoutException}. This will not result in the + * promise being cancelled. + * + * @param time the amount of time to wait before timing out the promise + * @param unit the time unit of the delay + * @return continuation of the promise chain + */ @NotNull Promise maxWaitTime(long time, @NotNull TimeUnit unit); + /** + * Times out the promise if not already completed after the specified timeout. This will result + * in an exceptional completion with a {@link TimeoutException}. This will not result in the + * promise being cancelled. + * + * @param ms the amount of time to wait before timing out the promise (in milliseconds) + * @return continuation of the promise chain + */ default @NotNull Promise maxWaitTime(long ms) { return maxWaitTime(ms, TimeUnit.MILLISECONDS); } - void cancel(@Nullable String reason); + /** + * Cancels the promise if not already completed after the specified timeout. This will result in + * an exceptional completion with the specified cancellation. + * + * @param exception the cancellation exception to complete the promise with + */ + void cancel(@NotNull CancellationException exception); - default void cancel() { - cancel(null); + /** + * Cancels the promise if not already completed after the specified timeout. This will result in + * an exceptional completion with a {@link CancellationException}. + * + * @param reason the reason for the cancellation + */ + default void cancel(@NotNull String reason) { + cancel(new CancellationException(reason)); } - void complete(@Nullable T result); - - void completeExceptionally(@NotNull Throwable result); + /** + * Cancels the promise if not already completed after the specified timeout. This will result in + * an exceptional completion with a {@link CancellationException}. + */ + default void cancel() { + cancel(new CancellationException()); + } + /** + * Blocks until this promise has completed, and then returns its result. This method will throw + * checked exceptions if the promise completes exceptionally or the thread is interrupted. + * + * @return the result of the promise + * @throws CancellationException if the promise was cancelled + * @throws ExecutionException if the promise completed exceptionally + * @throws InterruptedException if the current thread was interrupted while waiting + */ @Blocking - T awaitInterruptibly() throws InterruptedException; + T get() throws InterruptedException, ExecutionException; + /** + * Blocks until either this promise has completed or the timeout has been exceeded, and then + * returns its result, if available. This method will throw checked exceptions if the promise + * completes exceptionally or the thread is interrupted, or the timeout is exceeded. + * + * @return the result of the promise + * @throws CancellationException if the promise was cancelled + * @throws ExecutionException if the promise completed exceptionally + * @throws InterruptedException if the current thread was interrupted while waiting + * @throws TimeoutException if the timeout was exceeded + */ @Blocking - T awaitInterruptibly(long timeout) throws TimeoutException, InterruptedException; + T get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; + /** + * Blocks until this promise has completed, and then returns its result. This method is similar + * to {@link #get()}, but will throw unchecked exceptions instead of checked exceptions if the + * promise completes exceptionally or the thread is interrupted. + * + * @return the result of the promise + * @throws CancellationException if the promise was cancelled + * @throws CompletionException if the promise completed exceptionally + */ @Blocking T await(); - @Blocking - T await(long timeout) throws TimeoutException; + /** + * Returns a new promise, backed by this promise, that will not propagate cancellations. This means + * that if the returned promise is cancelled, the cancellation will not be propagated to this promise, + * and consequently any previous promises in the chain. + * + * @return continuation the promise chain that will not propagate cancellations + */ + @NotNull Promise fork(); /** - * @deprecated Use await instead. + * Returns the current completion state of this promise. If the promise has not completed, this method + * will return {@code null}. + * + * @return the completion state of this promise, or {@code null} if the promise has not completed */ - @Blocking - @Deprecated - default T join(long timeout) throws TimeoutException { - return await(timeout); - }; - @Nullable PromiseCompletion getCompletion(); + /** + * Returns whether this promise has completed. + * + * @return {@code true} if the promise has completed, {@code false} otherwise + */ boolean isCompleted(); + /** + * Converts this promise to a {@link CompletableFuture}. The returned future will complete with the + * result of this promise when it completes. + * + * @return a future that will complete with the result of this promise + */ @NotNull CompletableFuture toFuture(); } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseCompletion.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseCompletion.java index de7e276..7ca6e9f 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseCompletion.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseCompletion.java @@ -5,35 +5,85 @@ import org.jetbrains.annotations.Nullable; import java.util.concurrent.CancellationException; +/** + * Represents the result of a {@link Promise}, containing either an optional result or an exception. + */ public class PromiseCompletion { private @Nullable T result; private @Nullable Throwable exception; + /** + * Creates a new successful completion. + * + * @param result the result + */ public PromiseCompletion(@Nullable T result) { this.result = result; } + /** + * Creates a new exceptional completion. + * + * @param exception the exception + */ public PromiseCompletion(@NotNull Throwable exception) { this.exception = exception; } + /** + * Creates a new successful completion with a result of {@code null}. + */ public PromiseCompletion() { - this.result = null; + this((T) null); } + /** + * Checks if the completion was successful. + * + * @return {@code true} if the completion was successful, {@code false} otherwise + */ + public boolean isSuccess() { + return exception == null; + } + + /** + * Checks if the completion was exceptional. + * + * @return {@code true} if the completion was exceptional, {@code false} otherwise + */ public boolean isError() { - return getException() != null; + return exception != null; } + /** + * Checks if the completion was cancelled. + * + * @return {@code true} if the completion was cancelled, {@code false} otherwise + */ + public boolean wasCancelled() { + return exception instanceof CancellationException; + } + + @Deprecated public boolean wasCanceled() { - return getException() instanceof CancellationException; + return wasCancelled(); } + /** + * Gets the result of the completion. + * + * @return the result, or {@code null} if the completion was exceptional + */ public @Nullable T getResult() { return result; } + /** + * Gets the exception of the completion. + * + * @return the exception, or {@code null} if the completion was successful + */ public @Nullable Throwable getException() { return exception; } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactory.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactory.java index d7a3d45..94ba74b 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactory.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactory.java @@ -1,100 +1,502 @@ package dev.tommyjs.futur.promise; +import dev.tommyjs.futur.executor.PromiseExecutor; +import dev.tommyjs.futur.util.PromiseUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +/** + * A factory for creating and combining promises. + */ +@SuppressWarnings("unchecked") public interface PromiseFactory { - @NotNull Logger getLogger(); - - @NotNull Promise unresolved(); - - @NotNull Promise> combine(boolean propagateCancel, @NotNull Promise p1, @NotNull Promise p2); - - default @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2) { - return combine(false, p1, p2); - } - - @NotNull Promise> combine(boolean propagateCancel, @NotNull Map> promises, @Nullable BiConsumer exceptionHandler); - - default @NotNull Promise> combine(boolean propagateCancel, @NotNull Map> promises) { - return combine(propagateCancel, promises, null); - } - - default @NotNull Promise> combine(@NotNull Map> promises, @Nullable BiConsumer exceptionHandler) { - return combine(false, promises, exceptionHandler); - } - - default @NotNull Promise> combine(@NotNull Map> promises) { - return combine(promises, null); - } - - @NotNull Promise> combine(boolean propagateCancel, @NotNull Iterable> promises, @Nullable BiConsumer exceptionHandler); - - default @NotNull Promise> combine(boolean propagateCancel, @NotNull Iterable> promises) { - return combine(propagateCancel, promises, null); - } - - default @NotNull Promise> combine(@NotNull Iterable> promises, @Nullable BiConsumer exceptionHandler) { - return combine(false, promises, exceptionHandler); - } - - default @NotNull Promise> combine(@NotNull Iterable> promises) { - return combine(promises, null); - } - - @NotNull Promise>> allSettled(boolean propagateCancel, @NotNull Iterable> promiseIterable); - - default @NotNull Promise>> allSettled(@NotNull Iterable> promiseIterable) { - return allSettled(false, promiseIterable); - } - - default @NotNull Promise>> allSettled(boolean propagateCancel, @NotNull Promise... promiseArray) { - return allSettled(propagateCancel, Arrays.asList(promiseArray)); - } - - default @NotNull Promise>> allSettled(@NotNull Promise... promiseArray) { - return allSettled(false, promiseArray); - } - - @NotNull Promise all(boolean propagateCancel, @NotNull Iterable> promiseIterable); - - default @NotNull Promise all(@NotNull Iterable> promiseIterable) { - return all(false, promiseIterable); - } - - default @NotNull Promise all(boolean propagateCancel, @NotNull Promise... promiseArray) { - return all(propagateCancel, Arrays.asList(promiseArray)); - } - - default @NotNull Promise all(@NotNull Promise... promiseArray) { - return all(false, promiseArray); + /** + * Creates a new {@link PromiseFactory} with the given logger and executors. + * + * @param logger the logger + * @param syncExecutor the synchronous executor + * @param asyncExecutor the asynchronous executor + * @return the new promise factory + */ + static @NotNull PromiseFactory of(@NotNull Logger logger, @NotNull PromiseExecutor syncExecutor, + @NotNull PromiseExecutor asyncExecutor) { + return new PromiseFactoryImpl<>(logger, syncExecutor, asyncExecutor); } /** - * @apiNote Even with cancelRaceLosers, it is not guaranteed that only one promise will complete. + * Creates a new {@link PromiseFactory} with the given logger and dual executor. + * + * @param logger the logger + * @param executor the executor + * @return the new promise factory */ - @NotNull Promise race(boolean cancelRaceLosers, @NotNull Iterable> promises); + static @NotNull PromiseFactory of(@NotNull Logger logger, @NotNull PromiseExecutor executor) { + return new PromiseFactoryImpl<>(logger, executor, executor); + } + /** + * Creates a new {@link PromiseFactory} with the given logger and executor. + * + * @param logger the logger + * @param executor the executor + * @return the new promise factory + */ + static @NotNull PromiseFactory of(@NotNull Logger logger, @NotNull ScheduledExecutorService executor) { + return of(logger, PromiseExecutor.of(executor)); + } + + /** + * Creates a new uncompleted promise. + * + * @return the new promise + */ + @NotNull CompletablePromise unresolved(); + + /** + * Creates a new promise, completed with the given value. + * + * @param value the value to complete the promise with + * @return the new promise + */ + @NotNull Promise resolve(T value); + + /** + * Creates a new promise, completed with {@code null}. This method is often useful for starting + * promise chains. + * + * @return the new promise + */ + @NotNull Promise start(); + + /** + * Creates a new promise, completed exceptionally with the given error. + * + * @param error the error to complete the promise with + * @return the new promise + */ + @NotNull Promise error(@NotNull Throwable error); + + /** + * Creates a new promise backed by the given completion and future. + * The promise will be completed upon completion of the {@link CompletionStage} + * and the {@link Future} will be cancelled upon cancellation of the promise. + * + * @param completion the completion stage to wrap + * @param future the future to wrap + * @return the new promise + */ + @NotNull Promise wrap(@NotNull CompletionStage completion, @Nullable Future future); + + /** + * Creates a new promise backed by the given future. + * The promise will be completed upon completion of the {@link CompletableFuture} + * and the {@link CompletableFuture} will be cancelled upon cancellation of the promise. + * + * @param future the future to wrap + * @return the new promise + */ + default @NotNull Promise wrap(@NotNull CompletableFuture future) { + return wrap(future, future); + }; + + /** + * Combines two promises into a single promise that resolves when both promises are completed. + * If either input promise completes exceptionally, the other promise will be cancelled + * and the output promise will complete exceptionally. + * + * @param p1 the first promise + * @param p2 the second promise + * @return the combined promise + */ + @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2); + + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + @NotNull Promise> combineMapped(@NotNull Iterator>> promises, + int expectedSize); + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Spliterator>> promises) { + return combineMapped(Spliterators.iterator(promises), PromiseUtil.estimateSize(promises)); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Stream>> promises) { + return combineMapped(promises.spliterator()); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Iterable>> promises) { + return combineMapped(promises.spliterator()); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Map.Entry>... promises) { + return combineMapped(Arrays.spliterator(promises)); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Map> promises) { + return combineMapped(promises.entrySet().iterator(), promises.size()); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param keys the keys to map to promises + * @param mapper the function to map keys to promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Stream keys, + @NotNull Function> mapper) { + return combineMapped(keys.map(k -> new AbstractMap.SimpleImmutableEntry<>(k, mapper.apply(k)))); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param keys the keys to map to promises + * @param mapper the function to map keys to promises + * @return the combined promise + */ + default @NotNull Promise> combineMapped(@NotNull Iterable keys, + @NotNull Function> mapper) { + return combineMapped(StreamSupport.stream(keys.spliterator(), false), mapper); + } + + /** + * @deprecated Use combineMapped instead. + */ + @Deprecated + default @NotNull Promise> combine(@NotNull Map> promises) { + return combineMapped(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + @NotNull Promise> combine(@NotNull Iterator> promises, int expectedSize); + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combine(@NotNull Spliterator> promises) { + return combine(Spliterators.iterator(promises), PromiseUtil.estimateSize(promises)); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combine(@NotNull Stream> promises) { + return combine(promises.spliterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combine(@NotNull Iterable> promises) { + return combine(promises.spliterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise> combine(@NotNull Promise... promises) { + return combine(Arrays.spliterator(promises)); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + @NotNull Promise>> allSettled(@NotNull Iterator> promises, + int expectedSize); + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise>> allSettled(@NotNull Spliterator> promises) { + return allSettled(Spliterators.iterator(promises), PromiseUtil.estimateSize(promises)); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise>> allSettled(@NotNull Stream> promises) { + return allSettled(promises.spliterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise>> allSettled(@NotNull Iterable> promises) { + return allSettled(promises.spliterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise>> allSettled(@NotNull Promise... promises) { + return allSettled(Arrays.spliterator(promises)); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + @NotNull Promise all(@NotNull Iterator> promises); + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise all(@NotNull Stream> promises) { + return all(promises.iterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise all(@NotNull Iterable> promises) { + return all(promises.iterator()); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise all(@NotNull Promise... promises) { + return all(Arrays.asList(promises).iterator()); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, if {@code cancelLosers} is {@code true}, the other promises will be cancelled + * once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + @NotNull Promise race(@NotNull Iterator> promises, boolean ignoreErrors); + + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + default @NotNull Promise race(@NotNull Stream> promises, boolean ignoreErrors) { + return race(promises.iterator(), ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise race(@NotNull Stream> promises) { + return race(promises, false); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + default @NotNull Promise race(@NotNull Iterable> promises, boolean ignoreErrors) { + return race(promises.iterator(), ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ default @NotNull Promise race(@NotNull Iterable> promises) { + return race(promises, false); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + default @NotNull Promise race(boolean ignoreErrors, @NotNull Promise... promises) { + return race(Arrays.asList(promises), ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ + default @NotNull Promise race(@NotNull Promise... promises) { return race(false, promises); } - @NotNull Promise wrap(@NotNull CompletableFuture future); - - default @NotNull Promise start() { - return resolve(null); - } - - @NotNull Promise resolve(T value); - - @NotNull Promise error(@NotNull Throwable error); - } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactoryImpl.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactoryImpl.java new file mode 100644 index 0000000..7604aeb --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseFactoryImpl.java @@ -0,0 +1,89 @@ +package dev.tommyjs.futur.promise; + +import dev.tommyjs.futur.executor.PromiseExecutor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +public class PromiseFactoryImpl extends AbstractPromiseFactory { + + private final @NotNull Logger logger; + private final @NotNull PromiseExecutor syncExecutor; + private final @NotNull PromiseExecutor asyncExecutor; + + public PromiseFactoryImpl( + @NotNull Logger logger, + @NotNull PromiseExecutor syncExecutor, + @NotNull PromiseExecutor asyncExecutor + ) { + this.logger = logger; + this.syncExecutor = syncExecutor; + this.asyncExecutor = asyncExecutor; + } + + @Override + public @NotNull CompletablePromise unresolved() { + return new PromiseImpl<>(); + } + + @Override + public @NotNull Promise resolve(T value) { + return new CompletedPromiseImpl<>(value); + } + + @Override + public @NotNull Promise start() { + return new CompletedPromiseImpl<>(); + } + + @Override + public @NotNull Promise error(@NotNull Throwable error) { + return new CompletedPromiseImpl<>(error); + } + + @Override + public @NotNull Logger getLogger() { + return logger; + } + + @Override + public @NotNull PromiseExecutor getSyncExecutor() { + return syncExecutor; + } + + @Override + public @NotNull PromiseExecutor getAsyncExecutor() { + return asyncExecutor; + } + + private class PromiseImpl extends BasePromise { + + @Override + public @NotNull AbstractPromiseFactory getFactory() { + return PromiseFactoryImpl.this; + } + + } + + private class CompletedPromiseImpl extends CompletedPromise { + + public CompletedPromiseImpl(@Nullable T result) { + super(new PromiseCompletion<>(result)); + } + + public CompletedPromiseImpl(@NotNull Throwable exception) { + super(new PromiseCompletion<>(exception)); + } + + public CompletedPromiseImpl() { + super(); + } + + @Override + public @NotNull AbstractPromiseFactory getFactory() { + return PromiseFactoryImpl.this; + } + + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseListener.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseListener.java index 7ec1a26..18666f1 100644 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseListener.java +++ b/futur-api/src/main/java/dev/tommyjs/futur/promise/PromiseListener.java @@ -2,8 +2,16 @@ package dev.tommyjs.futur.promise; import org.jetbrains.annotations.NotNull; +/** + * A listener for a {@link Promise} that is called when the promise is resolved. + */ public interface PromiseListener { - void handle(@NotNull PromiseCompletion ctx); + /** + * Handles the completion of the promise. + * + * @param completion the promise completion + */ + void handle(@NotNull PromiseCompletion completion); } diff --git a/futur-api/src/main/java/dev/tommyjs/futur/promise/Promises.java b/futur-api/src/main/java/dev/tommyjs/futur/promise/Promises.java deleted file mode 100644 index ff1f0b8..0000000 --- a/futur-api/src/main/java/dev/tommyjs/futur/promise/Promises.java +++ /dev/null @@ -1,90 +0,0 @@ -package dev.tommyjs.futur.promise; - -import dev.tommyjs.futur.function.ExceptionalFunction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; - -/** - * @deprecated Use PromiseFactory instance methods instead. - */ -@Deprecated -public class Promises { - - public static @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2) { - return combine(p1, p2, p1.getFactory()); - } - - public static @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2, PromiseFactory factory) { - return factory.combine(p1, p2); - } - - public static @NotNull Promise> combine(@NotNull Map> promises, long timeout, PromiseFactory factory) { - return combine(promises, timeout, true, factory); - } - - public static @NotNull Promise> combine(@NotNull Map> promises, long timeout, boolean strict, PromiseFactory factory) { - return combine(promises, timeout, strict ? null : (_k, _v) -> {}, factory); - } - - public static @NotNull Promise> combine(@NotNull Map> promises, long timeout, @Nullable BiConsumer exceptionHandler, PromiseFactory factory) { - return factory.combine(promises, exceptionHandler).timeout(timeout); - } - - public static @NotNull Promise> combine(@NotNull Map> promises, PromiseFactory factory) { - return combine(promises, 1500L, true, factory); - } - - public static @NotNull Promise> combine(@NotNull List> promises, long timeout, PromiseFactory factory) { - return combine(promises, timeout, true, factory); - } - - public static @NotNull Promise> combine(@NotNull List> promises, long timeout, boolean strict, PromiseFactory factory) { - return factory.combine(promises, strict ? null : (_i, _v) -> {}).timeout(timeout); - } - - public static @NotNull Promise> combine(@NotNull List> promises, PromiseFactory factory) { - return combine(promises, 1500L, true, factory); - } - - public static @NotNull Promise all(@NotNull List> promises, PromiseFactory factory) { - return factory.all(promises); - } - - public static @NotNull Promise> combine(@NotNull Collection keys, @NotNull ExceptionalFunction mapper, long timeout, PromiseFactory factory) { - return combine(keys, mapper, timeout, true, factory); - } - - public static @NotNull Promise> combine(@NotNull Collection keys, @NotNull ExceptionalFunction mapper, long timeout, boolean strict, PromiseFactory factory) { - Map> promises = new HashMap<>(); - for (K key : keys) { - Promise promise = factory.resolve(key).thenApplyAsync(mapper); - promises.put(key, promise); - } - - return combine(promises, timeout, strict, factory); - } - - public static @NotNull Promise> combine(@NotNull Collection keys, @NotNull ExceptionalFunction mapper, PromiseFactory factory) { - return combine(keys, mapper, 1500L, true, factory); - } - - public static @NotNull Promise erase(@NotNull Promise p) { - return erase(p, p.getFactory()); - } - - public static @NotNull Promise erase(@NotNull Promise p, PromiseFactory factory) { - return p.erase(); - } - - public static @NotNull Promise wrap(@NotNull CompletableFuture future, PromiseFactory factory) { - return factory.wrap(future); - } - -} \ No newline at end of file diff --git a/futur-api/src/main/java/dev/tommyjs/futur/util/ConcurrentResultArray.java b/futur-api/src/main/java/dev/tommyjs/futur/util/ConcurrentResultArray.java new file mode 100644 index 0000000..ec202f1 --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/util/ConcurrentResultArray.java @@ -0,0 +1,35 @@ +package dev.tommyjs.futur.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class ConcurrentResultArray { + + private static final float RESIZE_FACTOR = 1.2F; + + private final AtomicReference ref; + + public ConcurrentResultArray(int expectedSize) { + //noinspection unchecked + this.ref = new AtomicReference<>((T[]) new Object[expectedSize]); + } + + public void set(int index, T element) { + ref.updateAndGet(array -> { + if (array.length <= index) { + array = Arrays.copyOf(array, (int) (array.length * RESIZE_FACTOR)); + } + + array[index] = element; + return array; + }); + } + + public @NotNull List toList() { + return Arrays.asList(ref.get()); + } + +} diff --git a/futur-api/src/main/java/dev/tommyjs/futur/util/PromiseUtil.java b/futur-api/src/main/java/dev/tommyjs/futur/util/PromiseUtil.java new file mode 100644 index 0000000..f2a87bd --- /dev/null +++ b/futur-api/src/main/java/dev/tommyjs/futur/util/PromiseUtil.java @@ -0,0 +1,52 @@ +package dev.tommyjs.futur.util; + +import dev.tommyjs.futur.promise.CompletablePromise; +import dev.tommyjs.futur.promise.Promise; +import org.jetbrains.annotations.NotNull; + +import java.util.Spliterator; + +public class PromiseUtil { + + /** + * Propagates the completion, once completed, of the given promise to the given promise. + * + * @param from the promise to propagate the completion from + * @param to the completable promise to propagate the completion to + */ + public static void propagateCompletion(@NotNull Promise from, @NotNull CompletablePromise to) { + from.addDirectListener(to::complete, to::completeExceptionally); + } + + /** + * Propagates the cancellation, once cancelled, of the given promise to the given promise. + * + * @param from the promise to propagate the cancellation from + * @param to the promise to propagate the cancellation to + */ + public static void propagateCancel(@NotNull Promise from, @NotNull Promise to) { + from.onCancel(to::cancel); + } + + /** + * Cancels the given promise once the given promise is completed. + * + * @param from the promise to propagate the completion from + * @param to the promise to cancel upon completion + */ + public static void cancelOnComplete(@NotNull Promise from, @NotNull Promise to) { + from.addDirectListener(_ -> to.cancel()); + } + + /** + * Estimates the size of the given stream. + * + * @param stream the stream + * @return the estimated size + */ + public static int estimateSize(@NotNull Spliterator stream) { + long estimate = stream.estimateSize(); + return estimate == Long.MAX_VALUE ? 10 : (int) estimate; + } + +} diff --git a/futur-api/src/test/java/dev/tommyjs/futur/PromiseTests.java b/futur-api/src/test/java/dev/tommyjs/futur/PromiseTests.java index 6195924..5f3a8ca 100644 --- a/futur-api/src/test/java/dev/tommyjs/futur/PromiseTests.java +++ b/futur-api/src/test/java/dev/tommyjs/futur/PromiseTests.java @@ -1,7 +1,7 @@ package dev.tommyjs.futur; -import dev.tommyjs.futur.executor.SinglePoolExecutor; -import dev.tommyjs.futur.impl.SimplePromiseFactory; +import dev.tommyjs.futur.promise.CompletablePromise; +import dev.tommyjs.futur.promise.CompletedPromise; import dev.tommyjs.futur.promise.Promise; import dev.tommyjs.futur.promise.PromiseFactory; import org.junit.jupiter.api.Test; @@ -12,29 +12,43 @@ import java.util.List; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; public final class PromiseTests { private final Logger logger = LoggerFactory.getLogger(PromiseTests.class); - private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(5); - private final PromiseFactory pfac = new SimplePromiseFactory<>(new SinglePoolExecutor(executor), logger); + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(6); + private final PromiseFactory promises = PromiseFactory.of(logger, executor); + + @Test + public void testErrors() { + Promise promise = promises.start().thenSupplyAsync(() -> { + throw new StackOverflowError(); + }); + + try { + promise.await(); + } catch (CompletionException e) { + assert e.getCause() instanceof StackOverflowError; + } + } @Test public void testShutdown() { - executor.shutdown(); - Promise promise = pfac.resolve(null).thenSupplyAsync(() -> null); + executor.close(); + Promise promise = promises.resolve(null).thenSupplyAsync(() -> null); try { promise.await(); - } catch (RuntimeException e) { + } catch (CompletionException e) { assert e.getCause() instanceof RejectedExecutionException; } } @Test - public void testErrorCancellation() throws InterruptedException { + public void testCancellation() throws InterruptedException { var finished = new AtomicBoolean(); - pfac.start() - .thenRunDelayedAsync(() -> finished.set(true), 50, TimeUnit.MILLISECONDS) + promises.start().thenRunDelayedAsync(() -> finished.set(true), 50, TimeUnit.MILLISECONDS) .thenRunAsync(() -> {}) .cancel(); @@ -42,13 +56,25 @@ public final class PromiseTests { assert !finished.get(); } + @Test + public void testFork() throws InterruptedException { + var finished = new AtomicBoolean(); + promises.start() + .thenRunDelayedAsync(() -> finished.set(true), 50, TimeUnit.MILLISECONDS) + .fork() + .cancel(); + + Thread.sleep(100L); + assert finished.get(); + } + @Test public void testToFuture() throws InterruptedException { - assert pfac.resolve(true).toFuture().getNow(false); - assert pfac.error(new Exception("Test")).toFuture().isCompletedExceptionally(); + assert promises.resolve(true).toFuture().getNow(false); + assert promises.error(new Exception("Test")).toFuture().isCompletedExceptionally(); var finished = new AtomicBoolean(); - pfac.start() + promises.start() .thenRunDelayedAsync(() -> finished.set(true), 50, TimeUnit.MILLISECONDS) .toFuture() .cancel(true); @@ -58,86 +84,81 @@ public final class PromiseTests { } @Test - public void testCombineUtil() throws TimeoutException { - pfac.all( - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) + public void testCombineUtil() throws TimeoutException, ExecutionException, InterruptedException { + promises.all( + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) ) - .join(100L); + .get(100L, TimeUnit.MILLISECONDS); - pfac.allSettled( - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) + promises.allSettled( + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) ) - .join(100L); + .get(100L, TimeUnit.MILLISECONDS); - pfac.combine( - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) + promises.combine( + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) ) - .join(100L); + .get(100L, TimeUnit.MILLISECONDS); - pfac.combine( + promises.combine( List.of( - pfac.start().thenRunDelayedAsync(() -> {}, 49, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> {}, 51, TimeUnit.MILLISECONDS) + promises.start().thenRunDelayedAsync(() -> {}, 49, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> {}, 51, TimeUnit.MILLISECONDS) ) ) - .join(100L); + .get(100L, TimeUnit.MILLISECONDS); - pfac.combine( + promises.combineMapped( Map.of( - "a", pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), - "b", pfac.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) + "a", promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS), + "b", promises.start().thenRunDelayedAsync(() -> {}, 50, TimeUnit.MILLISECONDS) ) ) - .join(100L); + .get(100L, TimeUnit.MILLISECONDS); } @Test public void testCombineUtilPropagation() throws InterruptedException { var finished1 = new AtomicBoolean(); - pfac.all( - true, - pfac.start().thenRunDelayedAsync(() -> finished1.set(true), 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> finished1.set(true), 50, TimeUnit.MILLISECONDS) + promises.all( + promises.start().thenRunDelayedAsync(() -> finished1.set(true), 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> finished1.set(true), 50, TimeUnit.MILLISECONDS) ) .cancel(); var finished2 = new AtomicBoolean(); - pfac.allSettled( - true, - pfac.start().thenRunDelayedAsync(() -> finished2.set(true), 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> finished2.set(true), 50, TimeUnit.MILLISECONDS) + promises.allSettled( + promises.start().thenRunDelayedAsync(() -> finished2.set(true), 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> finished2.set(true), 50, TimeUnit.MILLISECONDS) ) .cancel(); var finished3 = new AtomicBoolean(); - pfac.combine( - true, - pfac.start().thenRunDelayedAsync(() -> finished3.set(true), 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> finished3.set(true), 50, TimeUnit.MILLISECONDS) + promises.combine( + promises.start().thenRunDelayedAsync(() -> finished3.set(true), 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> finished3.set(true), 50, TimeUnit.MILLISECONDS) ) .cancel(); var finished4 = new AtomicBoolean(); - pfac.combine( - true, + promises.combine( List.of( - pfac.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS), - pfac.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS) + promises.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS), + promises.start().thenRunDelayedAsync(() -> finished4.set(true), 50, TimeUnit.MILLISECONDS) ) ) .cancel(); var finished5 = new AtomicBoolean(); - pfac.combine( - true, + promises.combineMapped( Map.of( - "a", pfac.start().thenRunDelayedAsync(() -> finished5.set(true), 50, TimeUnit.MILLISECONDS), - "b", pfac.start().thenRunDelayedAsync(() -> finished5.set(true), 50, TimeUnit.MILLISECONDS) + "a", promises.start().thenRunDelayedAsync(() -> finished5.set(true), 50, TimeUnit.MILLISECONDS), + "b", promises.start().thenRunDelayedAsync(() -> finished5.set(true), 50, TimeUnit.MILLISECONDS) ) ) .cancel(); @@ -151,13 +172,87 @@ public final class PromiseTests { } @Test - public void testRace() throws TimeoutException { - assert pfac.race( + public void testRace() { + assert promises.race( List.of( - pfac.start().thenSupplyDelayedAsync(() -> true, 150, TimeUnit.MILLISECONDS), - pfac.start().thenSupplyDelayedAsync(() -> false, 200, TimeUnit.MILLISECONDS) + promises.start().thenSupplyDelayedAsync(() -> true, 150, TimeUnit.MILLISECONDS), + promises.start().thenSupplyDelayedAsync(() -> false, 200, TimeUnit.MILLISECONDS) ) - ).join(300L); + ).await(); + } + + @Test + public void testOrDefault() { + CompletablePromise promise = promises.unresolved(); + AtomicReference res = new AtomicReference<>(); + promise.orDefault(10).thenPopulateReference(res); + promise.completeExceptionally(new IllegalStateException("Test")); + assert res.get() == 10; + } + + @Test + public void testOrDefaultSupplier() { + CompletablePromise promise = promises.unresolved(); + AtomicReference res = new AtomicReference<>(); + promise.orDefault(() -> 10).thenPopulateReference(res); + promise.completeExceptionally(new IllegalStateException("Test")); + assert res.get() == 10; + } + + @Test + public void testOrDefaultFunction() { + CompletablePromise promise = promises.unresolved(); + AtomicReference res = new AtomicReference<>(); + promise.orDefault(e -> { + assert e instanceof IllegalStateException; + return 10; + }).thenPopulateReference(res); + promise.completeExceptionally(new IllegalStateException("Test")); + assert res.get() == 10; + } + + @Test + public void testOrDefaultError() { + CompletablePromise promise = promises.unresolved(); + AtomicReference res = new AtomicReference<>(); + Promise promise2 = promise.orDefault(e -> { + throw new IllegalStateException("Test"); + }).thenPopulateReference(res); + promise.completeExceptionally(new IllegalStateException("Test")); + + assert res.get() == null; + assert promise2.getCompletion() != null && promise2.getCompletion().getException() instanceof IllegalStateException; + } + + @Test + public void testStream() { + var res = promises.combine(Stream.of(1, 2, 3).map(promises::resolve)).await(); + assert res.size() == 3; + } + + @Test + public void combineMappedTest() { + var res = promises.combineMapped(List.of(1, 2, 3), + n -> promises.start().thenSupplyDelayedAsync(() -> n * 2, 50, TimeUnit.MILLISECONDS) + ).await(); + + assert res.size() == 3; + assert res.get(1) == 2; + assert res.get(2) == 4; + assert res.get(3) == 6; + } + + @Test + public void testImmediate1() { + var promise = promises.start().thenSupply(() -> 10); + assert promise.isCompleted() && promise instanceof CompletedPromise; + } + + @Test + public void testImmediate2() { + var resolved = promises.resolve(10); + var promise = promises.start().thenCompose(_ -> resolved); + assert promise.isCompleted() && promise instanceof CompletedPromise; } } diff --git a/futur-lazy/build.gradle b/futur-lazy/build.gradle new file mode 100644 index 0000000..78d5335 --- /dev/null +++ b/futur-lazy/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':futur-api') +} \ No newline at end of file diff --git a/futur-lazy/generator/bun.lockb b/futur-lazy/generator/bun.lockb new file mode 100644 index 0000000..265b64a Binary files /dev/null and b/futur-lazy/generator/bun.lockb differ diff --git a/futur-lazy/generator/index.ts b/futur-lazy/generator/index.ts new file mode 100644 index 0000000..ea74d6d --- /dev/null +++ b/futur-lazy/generator/index.ts @@ -0,0 +1,33 @@ +const PACKAGE = "src/main/java/dev/tommyjs/futur" +const SIGNAL = "// Generated delegates to static factory" + +const regex = /( {4}\/\*\*.+?)?((?:\S| )+? )(\S+)(\(.*?\))(?: {.+?}|;)/gs +const content = await Bun.file(`../../futur-api/${PACKAGE}/promise/PromiseFactory.java`).text() + +const methods = [""] +for (const match of content.matchAll(regex)) { + let [_, docs, head, name, params] = match + + head = head.trimStart() + if (head.startsWith("static")) continue + if (head.startsWith("default")) head = head.slice(8); + + const args = Array.from(params.matchAll(/ ([a-zA-Z1-9]+)[,)]/gs)) + .map(v => v[1]).join(", ") + + methods.push( + [ + `${docs} public static ${head}${name}${params} {`, + ` return factory.${name}(${args});`, + " }" + ].join("\n") + ) +} + +const output = Bun.file(`../${PACKAGE}/lazy/Promises.java`) + +const existing = await output.text() +const cutIndex = existing.indexOf(SIGNAL) + SIGNAL.length; +const newContent = existing.slice(0, cutIndex) + methods.join("\n\n") + "\n\n}" + +await Bun.write(output, newContent) \ No newline at end of file diff --git a/futur-lazy/generator/package.json b/futur-lazy/generator/package.json new file mode 100644 index 0000000..56136ad --- /dev/null +++ b/futur-lazy/generator/package.json @@ -0,0 +1,11 @@ +{ + "name": "generate-promises", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "latest" + } +} \ No newline at end of file diff --git a/futur-lazy/generator/tsconfig.json b/futur-lazy/generator/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/futur-lazy/generator/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/futur-lazy/src/main/java/dev/tommyjs/futur/lazy/Promises.java b/futur-lazy/src/main/java/dev/tommyjs/futur/lazy/Promises.java new file mode 100644 index 0000000..e965478 --- /dev/null +++ b/futur-lazy/src/main/java/dev/tommyjs/futur/lazy/Promises.java @@ -0,0 +1,498 @@ +package dev.tommyjs.futur.lazy; + +import dev.tommyjs.futur.executor.PromiseExecutor; +import dev.tommyjs.futur.promise.CompletablePromise; +import dev.tommyjs.futur.promise.Promise; +import dev.tommyjs.futur.promise.PromiseCompletion; +import dev.tommyjs.futur.promise.PromiseFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Spliterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Stream; + +@SuppressWarnings("unchecked") +public final class Promises { + + private static final Logger LOGGER = LoggerFactory.getLogger(Promises.class); + private static PromiseFactory factory = PromiseFactory.of(LOGGER, PromiseExecutor.virtualThreaded()); + + public static void useFactory(@NotNull PromiseFactory factory) { + Promises.factory = factory; + } + + // Generated delegates to static factory + + /** + * Creates a new uncompleted promise. + * + * @return the new promise + */ + public static @NotNull CompletablePromise unresolved() { + return factory.unresolved(); + } + + /** + * Creates a new promise, completed with the given value. + * + * @param value the value to complete the promise with + * @return the new promise + */ + public static @NotNull Promise resolve(T value) { + return factory.resolve(value); + } + + /** + * Creates a new promise, completed with {@code null}. This method is often useful for starting + * promise chains. + * + * @return the new promise + */ + public static @NotNull Promise start() { + return factory.start(); + } + + /** + * Creates a new promise, completed exceptionally with the given error. + * + * @param error the error to complete the promise with + * @return the new promise + */ + public static @NotNull Promise error(@NotNull Throwable error) { + return factory.error(error); + } + + /** + * Creates a new promise backed by the given completion and future. + * The promise will be completed upon completion of the {@link CompletionStage} + * and the {@link Future} will be cancelled upon cancellation of the promise. + * + * @param completion the completion stage to wrap + * @param future the future to wrap + * @return the new promise + */ + public static @NotNull Promise wrap(@NotNull CompletionStage completion, @Nullable Future future) { + return factory.wrap(completion, future); + } + + /** + * Creates a new promise backed by the given future. + * The promise will be completed upon completion of the {@link CompletableFuture} + * and the {@link CompletableFuture} will be cancelled upon cancellation of the promise. + * + * @param future the future to wrap + * @return the new promise + */ + public static @NotNull Promise wrap(@NotNull CompletableFuture future) { + return factory.wrap(future); + } + + /** + * Combines two promises into a single promise that resolves when both promises are completed. + * If either input promise completes exceptionally, the other promise will be cancelled + * and the output promise will complete exceptionally. + * + * @param p1 the first promise + * @param p2 the second promise + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2) { + return factory.combine(p1, p2); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Iterator>> promises, + int expectedSize) { + return factory.combineMapped(promises, expectedSize); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Spliterator>> promises) { + return factory.combineMapped(promises); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Stream>> promises) { + return factory.combineMapped(promises); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Iterable>> promises) { + return factory.combineMapped(promises); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Map.Entry>... promises) { + return factory.combineMapped(promises); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Map> promises) { + return factory.combineMapped(promises); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param keys the keys to map to promises + * @param mapper the function to map keys to promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Stream keys, + @NotNull Function> mapper) { + return factory.combineMapped(keys, mapper); + } + + /** + * Combines key-value pairs of promises into a single promise that completes + * when all promises are completed, with the results mapped by their keys. + * If any promise completes exceptionally, the other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param keys the keys to map to promises + * @param mapper the function to map keys to promises + * @return the combined promise + */ + public static @NotNull Promise> combineMapped(@NotNull Iterable keys, + @NotNull Function> mapper) { + return factory.combineMapped(keys, mapper); + } + + /** + * @deprecated Use combineMapped instead. + */ + @Deprecated + public static @NotNull Promise> combine(@NotNull Map> promises) { + return factory.combine(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Iterator> promises, int expectedSize) { + return factory.combine(promises, expectedSize); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Spliterator> promises) { + return factory.combine(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Stream> promises) { + return factory.combine(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Iterable> promises) { + return factory.combine(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of results in the original order. + * If any promise completes exceptionally, all other promises will be cancelled + * and the combined promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise> combine(@NotNull Promise... promises) { + return factory.combine(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @param expectedSize the expected size of the iterator (used for optimization) + * @return the combined promise + */ + public static @NotNull Promise>> allSettled(@NotNull Iterator> promises, + int expectedSize) { + return factory.allSettled(promises, expectedSize); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise>> allSettled(@NotNull Spliterator> promises) { + return factory.allSettled(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise>> allSettled(@NotNull Stream> promises) { + return factory.allSettled(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise>> allSettled(@NotNull Iterable> promises) { + return factory.allSettled(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises + * are completed, with a list of completions in the original order. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise>> allSettled(@NotNull Promise... promises) { + return factory.allSettled(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise all(@NotNull Iterator> promises) { + return factory.all(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise all(@NotNull Stream> promises) { + return factory.all(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise all(@NotNull Iterable> promises) { + return factory.all(promises); + } + + /** + * Combines multiple promises into a single promise that completes when all promises complete. + * If any promise completes exceptionally, all other promises will be cancelled + * and the output promise will complete exceptionally. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise all(@NotNull Promise... promises) { + return factory.all(promises); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, if {@code cancelLosers} is {@code true}, the other promises will be cancelled + * once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Iterator> promises, boolean ignoreErrors) { + return factory.race(promises, ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Stream> promises, boolean ignoreErrors) { + return factory.race(promises, ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Stream> promises) { + return factory.race(promises); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Iterable> promises, boolean ignoreErrors) { + return factory.race(promises, ignoreErrors); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Iterable> promises) { + return factory.race(promises); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If {@code ignoreErrors} is {@code false} and the first promise completed exceptionally, the + * combined promise will also complete exceptionally. Otherwise, the combined promise will wait for a + * successful completion or complete with {@code null} if all promises complete exceptionally. + * Additionally, The other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @param ignoreErrors whether to ignore promises that complete exceptionally + * @return the combined promise + */ + public static @NotNull Promise race(boolean ignoreErrors, @NotNull Promise... promises) { + return factory.race(ignoreErrors, promises); + } + + /** + * Combines multiple promises into a single promise that completes when any promise is completed. + * If the first promise completed exceptionally, the combined promise will also complete exceptionally. + * Additionally, the other promises will be cancelled once the combined promise is completed. + * + * @param promises the input promises + * @return the combined promise + */ + public static @NotNull Promise race(@NotNull Promise... promises) { + return factory.race(promises); + } + +} \ No newline at end of file diff --git a/futur-static/build.gradle b/futur-static/build.gradle deleted file mode 100644 index a60942c..0000000 --- a/futur-static/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -apply plugin: 'java-library' - -dependencies { - api project(':futur-api') - testImplementation project(':futur-api') -} \ No newline at end of file diff --git a/futur-static/src/main/java/dev/tommyjs/futur/lazy/PromiseUtil.java b/futur-static/src/main/java/dev/tommyjs/futur/lazy/PromiseUtil.java deleted file mode 100644 index abd2bdd..0000000 --- a/futur-static/src/main/java/dev/tommyjs/futur/lazy/PromiseUtil.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.tommyjs.futur.lazy; - -import dev.tommyjs.futur.promise.Promise; -import dev.tommyjs.futur.promise.PromiseCompletion; -import dev.tommyjs.futur.promise.PromiseFactory; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; - -public final class PromiseUtil { - - private static PromiseFactory pfac = StaticPromiseFactory.INSTANCE; - - public static @NotNull Logger getLogger() { - return pfac.getLogger(); - } - - public static void setPromiseFactory(PromiseFactory pfac) { - PromiseUtil.pfac = pfac; - } - - public static @NotNull Promise unresolved() { - return pfac.unresolved(); - } - - public static @NotNull Promise> combine(boolean propagateCancel, @NotNull Promise p1, @NotNull Promise p2) { - return pfac.combine(propagateCancel, p1, p2); - } - - public static @NotNull Promise> combine(@NotNull Promise p1, @NotNull Promise p2) { - return pfac.combine(p1, p2); - } - - public static @NotNull Promise> combine(boolean propagateCancel, @NotNull Map> promises, @Nullable BiConsumer exceptionHandler) { - return pfac.combine(propagateCancel, promises, exceptionHandler); - } - - public static @NotNull Promise> combine(@NotNull Map> promises, @Nullable BiConsumer exceptionHandler) { - return pfac.combine(promises, exceptionHandler); - } - - public static @NotNull Promise> combine(boolean propagateCancel, @NotNull Map> promises) { - return pfac.combine(propagateCancel, promises); - } - - public static @NotNull Promise> combine(@NotNull Map> promises) { - return pfac.combine(promises); - } - - public static @NotNull Promise> combine(boolean propagateCancel, @NotNull Iterable> promises, @Nullable BiConsumer exceptionHandler) { - return pfac.combine(propagateCancel, promises, exceptionHandler); - } - - public static @NotNull Promise> combine(@NotNull Iterable> promises, @Nullable BiConsumer exceptionHandler) { - return pfac.combine(promises, exceptionHandler); - } - - public static @NotNull Promise> combine(boolean propagateCancel, @NotNull Iterable> promises) { - return pfac.combine(propagateCancel, promises); - } - - public static @NotNull Promise> combine(@NotNull Iterable> promises) { - return pfac.combine(promises); - } - - public static @NotNull Promise>> allSettled(boolean propagateCancel, @NotNull Iterable> promiseIterable) { - return pfac.allSettled(propagateCancel, promiseIterable); - } - - public static @NotNull Promise>> allSettled(@NotNull Iterable> promiseIterable) { - return pfac.allSettled(promiseIterable); - } - - public static @NotNull Promise>> allSettled(boolean propagateCancel, @NotNull Promise... promiseArray) { - return pfac.allSettled(propagateCancel, promiseArray); - } - - public static @NotNull Promise>> allSettled(@NotNull Promise... promiseArray) { - return pfac.allSettled(promiseArray); - } - - public static @NotNull Promise all(boolean propagateCancel, @NotNull Iterable> promiseIterable) { - return pfac.all(propagateCancel, promiseIterable); - } - - public static @NotNull Promise all(@NotNull Iterable> promiseIterable) { - return pfac.all(promiseIterable); - } - - public static @NotNull Promise all(boolean propagateCancel, @NotNull Promise... promiseArray) { - return pfac.all(propagateCancel, promiseArray); - } - - public static @NotNull Promise all(@NotNull Promise... promiseArray) { - return pfac.all(promiseArray); - } - - public static @NotNull Promise race(@NotNull Iterable> promises) { - return pfac.race(promises); - } - - public static @NotNull Promise race(boolean cancelRaceLosers, @NotNull Iterable> promises) { - return pfac.race(cancelRaceLosers, promises); - } - - public static @NotNull Promise wrap(@NotNull CompletableFuture future) { - return pfac.wrap(future); - } - - public static @NotNull Promise resolve(T value) { - return pfac.resolve(value); - } - - public static @NotNull Promise start() { - return pfac.start(); - } - - public static @NotNull Promise error(@NotNull Throwable error) { - return pfac.error(error); - } - -} diff --git a/futur-static/src/main/java/dev/tommyjs/futur/lazy/StaticPromiseFactory.java b/futur-static/src/main/java/dev/tommyjs/futur/lazy/StaticPromiseFactory.java deleted file mode 100644 index 340b92b..0000000 --- a/futur-static/src/main/java/dev/tommyjs/futur/lazy/StaticPromiseFactory.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.tommyjs.futur.lazy; - -import dev.tommyjs.futur.executor.PromiseExecutor; -import dev.tommyjs.futur.executor.SinglePoolExecutor; -import dev.tommyjs.futur.impl.SimplePromise; -import dev.tommyjs.futur.promise.AbstractPromiseFactory; -import dev.tommyjs.futur.promise.Promise; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.Future; - -public final class StaticPromiseFactory extends AbstractPromiseFactory> { - - public final static StaticPromiseFactory INSTANCE = new StaticPromiseFactory(); - private final static @NotNull SinglePoolExecutor EXECUTOR = SinglePoolExecutor.create(1); - private final static @NotNull Logger LOGGER = LoggerFactory.getLogger(StaticPromiseFactory.class); - - private StaticPromiseFactory() { - } - - @Override - public @NotNull Logger getLogger() { - return LOGGER; - } - - @Override - public @NotNull Promise unresolved() { - return new SimplePromise<>(this); - } - - @Override - public @NotNull PromiseExecutor> getExecutor() { - return EXECUTOR; - } - -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20db9ad..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d4..f3b75f3 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +133,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +200,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle b/settings.gradle index 9d69470..a0e7d05 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ rootProject.name = 'futur' include 'futur-api' -include 'futur-static' +include 'futur-lazy'