the type of item emitted by the publisher
21 | * @param the type of the publisher
22 | * @param publisher the publisher to subscribe to for retrieving items asynchronously
23 | * @param mapResult function that will set generator's result
24 | * @return an {@code AsyncGenerator} that emits items from the publisher
25 | */
26 | @SuppressWarnings("unchecked")
27 | static , R> AsyncGenerator.Cancellable fromPublisher( P publisher, Supplier mapResult ) {
28 | var queue = new LinkedBlockingQueue>();
29 | return new GeneratorSubscriber<>( publisher, (Supplier) mapResult, queue );
30 | }
31 |
32 | /**
33 | * Creates an {@code AsyncGenerator} from a {@code Flow.Publisher}.
34 | *
35 | * @param the type of item emitted by the publisher
36 | * @param the type of the publisher
37 | * @param publisher the publisher to subscribe to for retrieving items asynchronously
38 | * @return an {@code AsyncGenerator} that emits items from the publisher
39 | */
40 | static > AsyncGenerator.Cancellable fromPublisher( P publisher ) {
41 | return fromPublisher( publisher, null );
42 | }
43 |
44 | /**
45 | * Converts an {@code AsyncGenerator} into a {@code Flow.Publisher}.
46 | *
47 | * @param the type of elements emitted by the publisher
48 | * @param generator the async generator to convert
49 | * @return a flow publisher
50 | */
51 | static Flow.Publisher toPublisher( AsyncGenerator generator ) {
52 | return new GeneratorPublisher<>( generator );
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main/java/org/bsc/async/internal/reactive/GeneratorPublisher.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async.internal.reactive;
2 |
3 | import org.bsc.async.AsyncGenerator;
4 |
5 | import java.util.concurrent.Flow;
6 |
7 | /**
8 | * A {@code GeneratorPublisher} is a {@link Flow.Publisher} that
9 | * generates items from an asynchronous generator.
10 | *
11 | * @param the type of items to be published
12 | */
13 | public class GeneratorPublisher implements Flow.Publisher {
14 |
15 | private final AsyncGenerator extends T> delegate;
16 |
17 | /**
18 | * Constructs a new GeneratorPublisher with the specified async generator.
19 | *
20 | * @param delegate The async generator to be used by this publisher.
21 | */
22 | public GeneratorPublisher(AsyncGenerator extends T> delegate) {
23 | this.delegate = delegate;
24 | }
25 |
26 | /**
27 | * Subscribes the provided {@code Flow.Subscriber} to this signal. The subscriber receives initial subscription,
28 | * handles asynchronous data flow, and manages any errors or completion signals.
29 | *
30 | * @param subscriber The subscriber to which the signal will be delivered.
31 | */
32 | @Override
33 | public void subscribe(Flow.Subscriber super T> subscriber) {
34 | subscriber.onSubscribe(new Flow.Subscription() {
35 | /**
36 | * Requests more elements from the upstream Publisher.
37 | *
38 | * The Publisher calls this method to indicate that it wants more items. The parameter {@code n}
39 | * specifies the number of additional items requested.
40 | *
41 | * @param n the number of items to request, a count greater than zero
42 | */
43 | @Override
44 | public void request(long n) {
45 | }
46 |
47 | /**
48 | * Cancels the operation.
49 | *
50 | */
51 | @Override
52 | public void cancel() {
53 | if( delegate instanceof AsyncGenerator.Cancellable> isCancellable ) {
54 | isCancellable.cancel( true );
55 | }
56 | }
57 | });
58 |
59 | delegate.forEachAsync(subscriber::onNext)
60 | .thenAccept( value -> {
61 | subscriber.onComplete();
62 | })
63 | .exceptionally( ex -> {
64 | subscriber.onError(ex);
65 | return null;
66 | });
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/changelog.mustache:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | {{#tags}}
4 |
5 |
6 | {{!#ifReleaseTag .}}
7 |
8 | {{!/ifReleaseTag}}
9 |
10 | ## [{{name}}](https://github.com/bsorrentino/java-async-generator/releases/tag/{{name}}) ({{tagDate .}})
11 |
12 | {{#ifContainsType commits type='feat'}}
13 | ### Features
14 |
15 | {{#commits}}
16 | {{#ifCommitType . type='feat'}}
17 | * {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
18 | {{#messageBodyItems}}
19 | > {{.}}
20 | {{/messageBodyItems}}
21 |
22 | {{/ifCommitType}}
23 | {{/commits}}
24 | {{/ifContainsType}}
25 |
26 | {{#ifContainsType commits type='fix'}}
27 | ### Bug Fixes
28 |
29 | {{#commits}}
30 | {{#ifCommitType . type='fix'}}
31 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
32 | {{#messageBodyItems}}
33 | > {{.}}
34 | {{/messageBodyItems}}
35 |
36 | {{/ifCommitType}}
37 | {{/commits}}
38 | {{/ifContainsType}}
39 |
40 | {{#ifContainsType commits type='docs'}}
41 | ### Documentation
42 |
43 | {{#commits}}
44 | {{#ifCommitType . type='docs'}}
45 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
46 | {{#messageBodyItems}}
47 | > {{.}}
48 | {{/messageBodyItems}}
49 |
50 | {{/ifCommitType}}
51 | {{/commits}}
52 | {{/ifContainsType}}
53 |
54 | {{#ifContainsType commits type='refactor'}}
55 | ### Refactor
56 |
57 | {{#commits}}
58 | {{#ifCommitType . type='refactor'}}
59 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
60 | {{#messageBodyItems}} > {{.}}
61 | {{/messageBodyItems}}
62 |
63 | {{/ifCommitType}}
64 | {{/commits}}
65 | {{/ifContainsType}}
66 |
67 | {{#ifContainsType commits type='build'}}
68 | ### ALM
69 |
70 | {{#commits}}
71 | {{#ifCommitType . type='build'}}
72 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
73 | {{#messageBodyItems}} > {{.}}
74 | {{/messageBodyItems}}
75 |
76 | {{/ifCommitType}}
77 | {{/commits}}
78 | {{/ifContainsType}}
79 |
80 | {{#ifContainsType commits type='test'}}
81 | ### Test
82 |
83 | {{#commits}}
84 | {{#ifCommitType . type='test'}}
85 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
86 | {{#messageBodyItems}} > {{.}}
87 | {{/messageBodyItems}}
88 |
89 | {{/ifCommitType}}
90 | {{/commits}}
91 | {{/ifContainsType}}
92 |
93 | {{#ifContainsType commits type='ci'}}
94 | ### Continuous Integration
95 |
96 | {{#commits}}
97 | {{#ifCommitType . type='ci'}}
98 | - {{#eachCommitScope .}} **{{.}}** {{/eachCommitScope}} {{{commitDescription .}}} ([{{hash}}](https://github.com/bsorrentino/java-async-generator/commit/{{hashFull}}))
99 | {{#messageBodyItems}} > {{.}}
100 | {{/messageBodyItems}}
101 |
102 | {{/ifCommitType}}
103 | {{/commits}}
104 | {{/ifContainsType}}
105 |
106 |
107 | {{/tags}}
108 |
--------------------------------------------------------------------------------
/src/main/java/org/bsc/async/internal/UnmodifiableDeque.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async.internal;
2 |
3 | import java.util.Collection;
4 | import java.util.Deque;
5 | import java.util.Iterator;
6 |
7 | public class UnmodifiableDeque implements Deque {
8 | private final Deque deque;
9 |
10 | public UnmodifiableDeque(Deque deque) {
11 | this.deque = deque;
12 | }
13 |
14 | @Override
15 | public boolean add(T t) { throw new UnsupportedOperationException();}
16 |
17 | @Override
18 | public boolean offer(T t) { throw new UnsupportedOperationException(); }
19 |
20 | @Override
21 | public T remove() { throw new UnsupportedOperationException();}
22 |
23 | @Override
24 | public T poll() { throw new UnsupportedOperationException(); }
25 |
26 | @Override
27 | public T element() {
28 | return deque.element();
29 | }
30 |
31 | @Override
32 | public T peek() {
33 | return deque.peek();
34 | }
35 |
36 | @Override
37 | public void addFirst(T t) { throw new UnsupportedOperationException(); }
38 |
39 | @Override
40 | public void addLast(T t) { throw new UnsupportedOperationException(); }
41 |
42 | @Override
43 | public boolean offerFirst(T t) { throw new UnsupportedOperationException(); }
44 |
45 | @Override
46 | public boolean offerLast(T t) { throw new UnsupportedOperationException(); }
47 |
48 | @Override
49 | public T removeFirst() { throw new UnsupportedOperationException(); }
50 |
51 | @Override
52 | public T removeLast() { throw new UnsupportedOperationException(); }
53 |
54 | @Override
55 | public T pollFirst() { throw new UnsupportedOperationException(); }
56 |
57 | @Override
58 | public T pollLast() { throw new UnsupportedOperationException(); }
59 |
60 | @Override
61 | public T getFirst() { return deque.getFirst(); }
62 |
63 | @Override
64 | public T getLast() { return deque.getLast(); }
65 |
66 | @Override
67 | public T peekFirst() { return deque.peekFirst(); }
68 |
69 | @Override
70 | public T peekLast() { return deque.peekLast(); }
71 |
72 | @Override
73 | public boolean removeFirstOccurrence(Object o) { throw new UnsupportedOperationException(); }
74 |
75 | @Override
76 | public boolean removeLastOccurrence(Object o) { throw new UnsupportedOperationException(); }
77 |
78 | @Override
79 | public boolean addAll(Collection extends T> c) { throw new UnsupportedOperationException(); }
80 |
81 | @Override
82 | public void clear() { throw new UnsupportedOperationException(); }
83 |
84 | @Override
85 | public boolean retainAll(Collection> c) { throw new UnsupportedOperationException(); }
86 |
87 | @Override
88 | public boolean removeAll(Collection> c) { throw new UnsupportedOperationException(); }
89 |
90 | @Override
91 | public boolean containsAll(Collection> c) { return deque.containsAll(c); }
92 |
93 | @Override
94 | public boolean contains(Object o) { return deque.contains(o); }
95 |
96 | @Override
97 | public int size() { return deque.size(); }
98 |
99 | @Override
100 | public boolean isEmpty() { return deque.isEmpty(); }
101 |
102 | @Override
103 | public Iterator iterator() { return deque.iterator(); }
104 |
105 | @Override
106 | public Object[] toArray() { return deque.toArray(); }
107 |
108 | @Override
109 | public T1[] toArray(T1[] a) { return deque.toArray(a); }
110 |
111 | @Override
112 | public Iterator descendingIterator() { return deque.descendingIterator(); }
113 |
114 | @Override
115 | public void push(T t) { throw new UnsupportedOperationException(); }
116 |
117 | @Override
118 | public T pop() { throw new UnsupportedOperationException(); }
119 |
120 | @Override
121 | public boolean remove(Object o) { throw new UnsupportedOperationException(); }
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/src/test/java/org/bsc/async/FutureCancellationTest.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.concurrent.CancellationException;
6 | import java.util.concurrent.CompletableFuture;
7 | import java.util.concurrent.Executors;
8 | import java.util.concurrent.atomic.AtomicInteger;
9 |
10 | import static org.junit.jupiter.api.Assertions.*;
11 |
12 | public class FutureCancellationTest {
13 |
14 | @Test
15 | public void cancelFutureTest() throws Exception {
16 | var executedSteps = new AtomicInteger(0);
17 | var exec = Executors.newSingleThreadExecutor();
18 |
19 | var future = exec.submit(() -> {
20 | try {
21 | for( var i = 0 ; i < 1000; ++i ) {
22 | System.out.printf("%d ) Start Working...\n", executedSteps.get());
23 | Thread.sleep(200); // throws InterruptedException
24 | System.out.printf("%d ) End Working...\n", executedSteps.getAndIncrement());
25 | }
26 | } catch (InterruptedException e) {
27 | System.out.println("Interrupted!");
28 | Thread.currentThread().interrupt(); // restore flag
29 | }
30 | });
31 |
32 | Thread.sleep(1000);
33 | future.cancel(true); // sends interrupt
34 | exec.shutdown();
35 |
36 | assertTrue( future.isCancelled() );
37 | assertTrue( future.isDone() );
38 | assertEquals( 4, executedSteps.get() );
39 |
40 | }
41 |
42 | @Test
43 | public void cancelCompletableFutureTest() throws Exception {
44 | var executedSteps = new AtomicInteger(0);
45 | var exec = Executors.newSingleThreadExecutor();
46 |
47 | var future = CompletableFuture.runAsync(() -> {
48 | try {
49 | for( var i = 0 ; i < 1000; ++i ) {
50 | System.out.printf("%d ) Start Working...\n", executedSteps.get());
51 | Thread.sleep(200); // throws InterruptedException
52 | System.out.printf("%d ) End Working...\n", executedSteps.getAndIncrement());
53 | }
54 | } catch (Exception e) {
55 | System.out.println("Interrupted!");
56 | Thread.currentThread().interrupt(); // restore flag
57 | }
58 | }, exec);
59 |
60 | Thread.sleep(1000);
61 | future.cancel(true); // sends interrupt
62 |
63 | assertTrue( future.isCancelled() );
64 | assertTrue( future.isDone() );
65 | assertEquals( 4, executedSteps.get() );
66 |
67 | exec.shutdown();
68 |
69 | }
70 |
71 | @Test
72 | public void cancelCompletableFutureChainTest() throws Exception {
73 | var executedSteps = new AtomicInteger(0);
74 | var exec = Executors.newSingleThreadExecutor();
75 |
76 | var future = CompletableFuture.runAsync(() -> {
77 | try {
78 | for( var i = 0 ; i < 1000; ++i ) {
79 | System.out.printf("%d ) Start Working...\n", executedSteps.get());
80 | Thread.sleep(200); // throws InterruptedException
81 | System.out.printf("%d ) End Working...\n", executedSteps.getAndIncrement());
82 | }
83 | } catch (Exception e) {
84 | System.out.println("Interrupted!");
85 | Thread.currentThread().interrupt(); // restore flag
86 | }
87 | }, exec);
88 |
89 | CompletableFuture.runAsync( () -> {
90 | try {
91 | Thread.sleep(1000);
92 | future.cancel(true); // sends interrupt
93 | System.out.println( "Future cancelled");
94 | } catch (InterruptedException e) {
95 | throw new RuntimeException(e);
96 | }
97 | });
98 |
99 | assertThrowsExactly( CancellationException.class, future::join);
100 |
101 | assertTrue( future.isCancelled() );
102 | assertTrue( future.isDone() );
103 | assertEquals( 4, executedSteps.get() );
104 |
105 | exec.shutdown();
106 |
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/org/bsc/async/AsyncGeneratorQueue.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import java.util.Objects;
4 | import java.util.concurrent.BlockingQueue;
5 | import java.util.concurrent.CompletableFuture;
6 | import java.util.concurrent.Executor;
7 | import java.util.function.Consumer;
8 |
9 | import static java.util.concurrent.ForkJoinPool.commonPool;
10 | import static org.bsc.async.AsyncGenerator.*;
11 |
12 | /**
13 | * Represents a queue-based asynchronous generator.
14 | */
15 | public class AsyncGeneratorQueue {
16 |
17 | /**
18 | * Inner class to generate asynchronous elements from the queue.
19 | *
20 | * @param the type of elements in the queue
21 | */
22 | public static class Generator extends BaseCancellable {
23 |
24 | private volatile Thread executorThread = null;
25 | private volatile Data endData = null;
26 | private final java.util.concurrent.BlockingQueue> queue;
27 |
28 | /**
29 | * Constructs a Generator with the specified queue.
30 | *
31 | * @param queue the blocking queue to generate elements from
32 | */
33 | public Generator(java.util.concurrent.BlockingQueue> queue) {
34 | this.queue = queue;
35 | }
36 |
37 | public java.util.concurrent.BlockingQueue> queue() {
38 | return queue;
39 | }
40 |
41 | private boolean isEnded() {
42 | return endData != null;
43 | }
44 |
45 | /**
46 | * Retrieves the next element from the queue asynchronously.
47 | *
48 | * @return the next element from the queue
49 | */
50 | @Override
51 | public Data next() {
52 | if( isEnded() ) {
53 | return endData;
54 | }
55 | if(executorThread!=null) {
56 | endData = Data.error(new IllegalStateException("illegal concurrent next() invocation"));
57 | return endData;
58 | }
59 | executorThread = Thread.currentThread();
60 | try {
61 | Data value = queue.take();
62 | if (value.isDone()) {
63 | endData = value;
64 | }
65 | return value;
66 | } catch (InterruptedException e) {
67 | endData = Data.done(CANCELLED);
68 | return endData;
69 | }
70 | finally {
71 | executorThread = null;
72 | }
73 | }
74 |
75 | @Override
76 | public boolean cancel( boolean mayInterruptIfRunning ) {
77 | if( super.cancel(mayInterruptIfRunning) ) {
78 | if( executorThread != null ) {
79 | executorThread.interrupt();
80 | }
81 | return true;
82 | }
83 | return false;
84 | }
85 | }
86 |
87 | /**
88 | * Creates an AsyncGenerator from the provided blocking queue and consumer.
89 | *
90 | * @param the type of elements in the queue
91 | * @param the type of blocking queue
92 | * @param queue the blocking queue to generate elements from
93 | * @param consumer the consumer for processing elements from the queue
94 | * @return an AsyncGenerator instance
95 | */
96 | public static >> AsyncGenerator of(Q queue, Consumer consumer) {
97 | return of( queue, consumer, commonPool() );
98 | }
99 |
100 | /**
101 | * Creates an AsyncGenerator from the provided queue, executor, and consumer.
102 | *
103 | * @param the type of elements in the queue
104 | * @param the type of blocking queue
105 | * @param queue the blocking queue to generate elements from
106 | * @param consumer the consumer for processing elements from the queue
107 | * @param executor the executor for asynchronous processing
108 | * @return an AsyncGenerator instance
109 | */
110 | public static >> AsyncGenerator of(Q queue, Consumer consumer, Executor executor ) {
111 | Objects.requireNonNull(queue);
112 | Objects.requireNonNull(executor);
113 | Objects.requireNonNull(consumer);
114 |
115 | executor.execute( () -> {
116 | try {
117 | consumer.accept(queue);
118 | }
119 | catch( Throwable ex ) {
120 | CompletableFuture error = new CompletableFuture<>();
121 | error.completeExceptionally(ex);
122 | queue.add( AsyncGenerator.Data.of(error));
123 | }
124 | finally {
125 | queue.add(Data.done());
126 | }
127 |
128 | });
129 |
130 | return new Generator<>(queue);
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/CANCELLATION.md:
--------------------------------------------------------------------------------
1 | # AsyncGenerator Cancellation
2 |
3 | The `AsyncGenerator` provides a mechanism to cancel an ongoing iteration. This is particularly useful for long-running asynchronous sequences.
4 |
5 | ## IsCancellable Interface
6 |
7 | Cancellation is supported by generators that are compliant with the `IsCancellable` interface. This interface provides the core methods for cancellation:
8 |
9 | ```java
10 | interface IsCancellable {
11 | boolean isCancelled();
12 | boolean cancel(boolean mayInterruptIfRunning);
13 | }
14 | ```
15 |
16 | An `AsyncGenerator` can be made cancellable, for example, by wrapping it with `AsyncGenerator.WithResult` or by using a generator that extends `AsyncGenerator.BaseCancellable`.
17 |
18 | ## Cancellation Behavior
19 |
20 | The `cancel(boolean mayInterruptIfRunning)` method allows for two types of cancellation:
21 |
22 | ### 1. Graceful Cancellation
23 |
24 | When you invoke `cancel(false)`, the generator sets an internal "cancelled" flag to `true`. The iteration will not be immediately terminated. Instead, it will stop gracefully before processing the *next* element. This ensures that the current operation completes, but no new operations are started. This is useful when you want to allow the current asynchronous task to finish its work to avoid leaving the system in an inconsistent state.
25 |
26 | ### 2. Immediate Cancellation
27 |
28 | Invoking `cancel(true)` also sets the internal "cancelled" flag. In addition, it attempts to interrupt the underlying thread that is executing the iteration. This is a more forceful cancellation and can be useful when you need to stop a long-running or blocked operation immediately. This will typically result in an `InterruptedException` being thrown within the task's execution block.
29 |
30 | ## Threading and Iteration
31 |
32 | The cancellation behavior is closely tied to how the `AsyncGenerator` is consumed.
33 |
34 | ### Using forEachAsync(consumer)
35 |
36 | When you use `forEachAsync(consumer)`, the iteration is executed on a new, dedicated single-thread executor.
37 |
38 | - `cancel(false)` will cause the loop to terminate before the next element is processed.
39 | - `cancel(true)` will interrupt the dedicated thread, causing the `forEachAsync` `CompletableFuture` to complete exceptionally (often with an `InterruptedException`).
40 |
41 | **Example from `asyncGeneratorForEachCancelTest`:**
42 |
43 | ```java
44 | final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
45 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
46 | final var cancellableIt = new AsyncGenerator.WithResult<>(it);
47 |
48 | CompletableFuture.runAsync( () -> {
49 | try {
50 | Thread.sleep( 2000 );
51 | cancellableIt.cancel(true); // Interrupt the thread
52 | } catch (InterruptedException e) {
53 | throw new RuntimeException(e);
54 | }
55 | });
56 |
57 | var futureResult = cancellableIt.forEachAsync( value -> {
58 | try {
59 | Thread.sleep( 500 );
60 | forEachResult.add(value);
61 | } catch (InterruptedException e) {
62 | // The thread is interrupted here
63 | Thread.currentThread().interrupt();
64 | throw new CompletionException(e);
65 | }
66 | } ).exceptionally( throwable -> {
67 | assertInstanceOf( InterruptedException.class, throwable.getCause());
68 | return AsyncGenerator.Cancellable.CANCELLED;
69 | });
70 | ```
71 |
72 | ### Using iterator()
73 |
74 | When you use the standard `for-each` loop with an `AsyncGenerator` (which uses the `iterator()` method), the iteration runs on the *current* thread. The `iterator()` blocks on each call to `next()` until the `CompletableFuture` for that element is resolved.
75 |
76 | - `cancel(false)` will cause `hasNext()` to return `false` on the next check, effectively stopping the loop.
77 | - `cancel(true)` will also cause `hasNext()` to return `false`. Since the iteration is running on the calling thread, interrupting should not have any effect on the current thread.
78 |
79 | ### How check if iteration has been interrupted
80 |
81 | To check if an iteration has been interrupted, you can use the `isCancelled()` available on on your `IsCancellable` generator.
82 | This method will return `true` if `cancel()` has been called, regardless of the `mayInterruptIfRunning` parameter.
83 |
84 | ```java
85 | if (cancellableGenerator.isCancelled()) {
86 | // Logic to handle the cancellation
87 | }
88 | ```
89 |
90 | ## Summary
91 |
92 | In summary, `cancel(false)` provides a non-disruptive way to signal termination, while `cancel(true)` offers a more immediate stop by leveraging thread interruption, which is most effective with `forEachAsync`.
93 |
94 | # Conclusion
95 |
96 | We must understand that the cancellation is a cooperative game and to make it effective we must be aware of this. Anyway the `async-generator` library provides a base implementation to make this game easier to play.
97 |
98 | ## Next Version - AbortController
99 |
100 | n the next version we have planned tp implement an `AbortController` allowing to provide a way to interrupt asynchronous task that could spawn different threads
--------------------------------------------------------------------------------
/src/main/java/org/bsc/async/internal/reactive/GeneratorSubscriber.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async.internal.reactive;
2 |
3 | import org.bsc.async.AsyncGenerator;
4 | import org.bsc.async.AsyncGeneratorQueue;
5 |
6 | import java.util.Optional;
7 | import java.util.concurrent.BlockingQueue;
8 | import java.util.concurrent.Executor;
9 | import java.util.concurrent.Flow;
10 | import java.util.function.Supplier;
11 |
12 | import static java.util.Objects.requireNonNull;
13 |
14 | /**
15 | * Represents a subscriber for generating asynchronous data streams.
16 | *
17 | * This class implements the {@link Flow.Subscriber} and {@link AsyncGenerator} interfaces to handle data flow
18 | * and produce asynchronous data. It is designed to subscribe to a publisher, process incoming items,
19 | * and manage error and completion signals.
20 | *
21 | * @param The type of elements produced by this generator.
22 | */
23 | public class GeneratorSubscriber implements AsyncGenerator.Cancellable, Flow.Subscriber {
24 |
25 | private final Supplier mapResult;
26 | private Flow.Subscription subscription;
27 | private final AsyncGeneratorQueue.Generator delegate;
28 |
29 | public Optional> mapResult() {
30 | return Optional.ofNullable(mapResult);
31 | }
32 |
33 | /**
34 | * Constructs a new instance of {@code GeneratorSubscriber}.
35 | *
36 | * @param the type of the publisher, which must extend {@link Flow.Publisher}
37 | * @param mapResult function that will set generator's result
38 | * @param publisher the source publisher that will push data to this subscriber
39 | * @param queue the blocking queue used for storing asynchronous generator data
40 | */
41 | public
> GeneratorSubscriber(P publisher,
42 | Supplier mapResult,
43 | BlockingQueue> queue) {
44 | this.delegate = new AsyncGeneratorQueue.Generator<>( queue );
45 | this.mapResult = mapResult;
46 | publisher.subscribe(this);
47 | }
48 | /**
49 | * Constructs a new instance of {@code GeneratorSubscriber}.
50 | *
51 | * @param the type of the publisher, which must extend {@link Flow.Publisher}
52 | * @param publisher the source publisher that will push data to this subscriber
53 | * @param queue the blocking queue used for storing asynchronous generator data
54 | */
55 | public
> GeneratorSubscriber(P publisher, BlockingQueue> queue) {
56 | this( publisher, null, queue );
57 | }
58 |
59 | /**
60 | * Handles the subscription event from a Flux.
61 | *
62 | * This method is called when a subscription to the source {@link Flow} has been established.
63 | * The provided {@code Flow.Subscription} can be used to manage and control the flow of data emissions.
64 | *
65 | * @param subscription The subscription object representing this resource owner lifecycle. Used to signal that resources being subscribed to should not be released until this subscription is disposed.
66 | */
67 | @Override
68 | public void onSubscribe(Flow.Subscription subscription) {
69 | this.subscription = subscription;
70 | subscription.request(Long.MAX_VALUE);
71 | }
72 |
73 | /**
74 | * Passes the received item to the delegated queue as an {@link AsyncGenerator.Data} object.
75 | *
76 | * @param item The item to be processed and queued.
77 | */
78 | @Override
79 | public void onNext(T item) {
80 | delegate.queue().add( AsyncGenerator.Data.of( item ) );
81 | }
82 |
83 | /**
84 | * Handles an error by queuing it in the delegate's queue with an errored data.
85 | *
86 | * @param error The Throwable that represents the error to be handled.
87 | */
88 | @Override
89 | public void onError(Throwable error) {
90 | delegate.queue().add( AsyncGenerator.Data.error(error) );
91 | }
92 |
93 | /**
94 | * This method is called when the asynchronous operation is completed successfully.
95 | * It notifies the delegate that no more data will be provided by adding a done marker to the queue.
96 | */
97 | @Override
98 | public void onComplete() {
99 | delegate.queue().add(AsyncGenerator.Data.done( mapResult().map(Supplier::get).orElse(null)));
100 | }
101 |
102 | /**
103 | * Returns the next {@code Data} object from this iteration.
104 | *
105 | * @return the next element in the iteration, or null if there is no such element
106 | */
107 | @Override
108 | public Data next() {
109 | return delegate.next();
110 | }
111 |
112 | @Override
113 | public final Executor executor() {
114 | return delegate.executor();
115 | }
116 |
117 | @Override
118 | public boolean isCancelled() {
119 | return delegate.isCancelled();
120 | }
121 |
122 | @Override
123 | public boolean cancel( boolean mayInterruptIfRunning ) {
124 | requireNonNull( subscription, "subscription cannot be null");
125 | subscription.cancel();
126 | return delegate.cancel(mayInterruptIfRunning);
127 | }
128 | }
--------------------------------------------------------------------------------
/src/test/java/org/bsc/async/FlowGeneratorTest.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.concurrent.*;
8 |
9 | import static java.util.concurrent.CompletableFuture.completedFuture;
10 | import static java.util.concurrent.CompletableFuture.runAsync;
11 | import static org.junit.jupiter.api.Assertions.*;
12 |
13 | public class FlowGeneratorTest {
14 |
15 | @Test
16 | public void flowGeneratorSubscriberTest() throws Exception {
17 |
18 | var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(20);
19 |
20 | var publisher = new SubmissionPublisher();
21 |
22 | final var data = List.of( "e1", "e2", "e3", "e4", "e5" );
23 |
24 | var generator = FlowGenerator.fromPublisher(publisher);
25 |
26 | assertTrue( publisher.hasSubscribers() );
27 |
28 | var submitting = runAsync( () -> {
29 | data.stream().peek(System.out::println).forEach( publisher::submit );
30 | publisher.close();
31 | }, executor );
32 |
33 | final List result = new ArrayList<>();
34 | var iterating = generator.forEachAsync(result::add);
35 |
36 | CompletableFuture.allOf(iterating, submitting );
37 |
38 | assertEquals( data.size(), result.size() );
39 | assertIterableEquals( data, result );
40 |
41 | System.out.println("Core pool size: " + executor.getCorePoolSize());
42 | System.out.println("Largest pool size: " + executor.getLargestPoolSize());
43 | System.out.println("Active threads: " + executor.getActiveCount());
44 | System.out.println("Completed tasks: " + executor.getCompletedTaskCount());
45 |
46 | }
47 |
48 | @Test
49 | public void flowGeneratorPublisherTest() throws Exception {
50 |
51 | var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(20);
52 |
53 | var queue = new LinkedBlockingQueue>();
54 |
55 | final var data = List.of( "e1", "e2", "e3", "e4", "e5" );
56 |
57 | var generator = AsyncGeneratorQueue.of( queue, q -> {
58 |
59 | for( String value: data ) {
60 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
61 | }
62 | }, executor);
63 |
64 | var publisher = FlowGenerator.toPublisher( generator );
65 |
66 | var result = new ArrayList();
67 |
68 | publisher.subscribe(new Flow.Subscriber() {
69 | @Override
70 | public void onSubscribe(Flow.Subscription subscription) {
71 | subscription.request(Long.MAX_VALUE);
72 | }
73 |
74 | @Override
75 | public void onNext(String item) {
76 | result.add(item);
77 | }
78 |
79 | @Override
80 | public void onError(Throwable throwable) {
81 | System.out.println(throwable.getLocalizedMessage());
82 | }
83 |
84 | @Override
85 | public void onComplete() {
86 | System.out.println("Completed");
87 | }
88 | });
89 |
90 | generator.toCompletableFuture().join();
91 |
92 | assertEquals( data.size(), result.size() );
93 | assertIterableEquals( data, result );
94 |
95 | System.out.println("Core pool size: " + executor.getCorePoolSize());
96 | System.out.println("Largest pool size: " + executor.getLargestPoolSize());
97 | System.out.println("Active threads: " + executor.getActiveCount());
98 | System.out.println("Completed tasks: " + executor.getCompletedTaskCount());
99 |
100 | }
101 |
102 | @Test
103 | public void flowGeneratorSubscriberAndCancelTest() throws Exception {
104 |
105 | var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(20);
106 |
107 | var publisher = new SubmissionPublisher();
108 |
109 | final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
110 |
111 | var generator = FlowGenerator.fromPublisher(publisher);
112 |
113 | assertTrue( publisher.hasSubscribers() );
114 |
115 | var submitting = CompletableFuture.runAsync( () -> {
116 |
117 | try {
118 | for (String value : data) {
119 | System.out.printf("publishing: %s on thread[%s]\n", value, Thread.currentThread().getName());
120 | publisher.submit(value);
121 |
122 | Thread.sleep(1000);
123 | }
124 | } catch( InterruptedException e ) {
125 | throw new CompletionException(e);
126 | } finally{
127 | publisher.close();
128 |
129 | }
130 | }, executor );
131 |
132 | var cancelling = CompletableFuture.runAsync( () -> {
133 | try {
134 | System.out.printf("cancelling start on thread[%s]\n", Thread.currentThread().getName());
135 |
136 | Thread.sleep( 4000 );
137 |
138 | System.out.printf("generator cancelled: %s\n", generator.cancel( true) );
139 |
140 | } catch (InterruptedException e) {
141 | throw new RuntimeException(e);
142 | }
143 | }, executor );
144 |
145 |
146 | final List result = new ArrayList<>();
147 | var iterating = generator
148 | .forEachAsync( value -> {
149 | try {
150 | Thread.sleep(10);
151 | System.out.printf("received: %s on thread[%s]\n", value, Thread.currentThread().getName());
152 | result.add(value);
153 | } catch (InterruptedException e) {
154 | System.err.printf( "interrupted on thread[%s]\n", Thread.currentThread().getName() );
155 | throw new CompletionException(e);
156 | }
157 | });
158 |
159 |
160 | CompletableFuture.allOf(iterating, submitting, cancelling );
161 |
162 | assertEquals( 4, result.size() );
163 | assertIterableEquals( result, List.of( "e1", "e2", "e3", "e4") );
164 |
165 | System.out.println("Core pool size: " + executor.getCorePoolSize());
166 | System.out.println("Largest pool size: " + executor.getLargestPoolSize());
167 | System.out.println("Active threads: " + executor.getActiveCount());
168 | System.out.println("Completed tasks: " + executor.getCompletedTaskCount());
169 |
170 | }
171 |
172 | }
173 |
--------------------------------------------------------------------------------
/src/test/java/org/bsc/async/AsyncGeneratorQueueTest.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.List;
8 | import java.util.concurrent.*;
9 | import java.util.stream.Collectors;
10 |
11 | import static java.util.concurrent.CompletableFuture.completedFuture;
12 | import static java.util.concurrent.ForkJoinPool.commonPool;
13 | import static org.bsc.async.AsyncGenerator.Cancellable.CANCELLED;
14 | import static org.junit.jupiter.api.Assertions.*;
15 |
16 | public class AsyncGeneratorQueueTest {
17 |
18 | @Test
19 | public void asyncGeneratorForEachTest() throws Exception {
20 |
21 | final BlockingQueue> queue = new LinkedBlockingQueue<>();
22 |
23 | final String[] data = { "e1", "e2", "e3", "e4", "e5"};
24 |
25 | final AsyncGenerator it = AsyncGeneratorQueue.of( queue, q -> {
26 | for( String value: data ) {
27 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
28 | }
29 | });
30 |
31 | List forEachResult = new ArrayList<>();
32 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
33 | System.out.println( "Finished forEach");
34 | }).join();
35 |
36 | List iterationResult = new ArrayList<>();
37 | for (String i : it) {
38 | iterationResult.add(i);
39 | System.out.println(i);
40 | }
41 | System.out.println( "Finished iteration");
42 |
43 | assertEquals( data.length, forEachResult.size() );
44 | assertIterableEquals( Arrays.asList(data), forEachResult );
45 | assertEquals( 0, iterationResult.size() );
46 | }
47 | @Test
48 | public void asyncGeneratorIteratorTest() throws Exception {
49 |
50 | final BlockingQueue> queue = new LinkedBlockingQueue<>();
51 |
52 | final String[] data = { "e1", "e2", "e3", "e4", "e5"};
53 |
54 | final AsyncGenerator it = AsyncGeneratorQueue.of( queue, q -> {
55 | for( String value: data ) {
56 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
57 | }
58 | });
59 |
60 | List iterationResult = new ArrayList<>();
61 | for (String i : it) {
62 | iterationResult.add(i);
63 | System.out.println(i);
64 | }
65 | System.out.println( "Finished iteration " + iterationResult);
66 |
67 | List forEachResult = new ArrayList<>();
68 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
69 | System.out.println( "Finished forEach");
70 | }).join();
71 |
72 | assertEquals( data.length, iterationResult.size() );
73 | assertIterableEquals( Arrays.asList(data), iterationResult );
74 | assertEquals( 0, forEachResult.size() );
75 | }
76 | @Test
77 | public void asyncGeneratorStreamTest() throws Exception {
78 |
79 | final BlockingQueue> queue = new LinkedBlockingQueue<>();
80 |
81 | final String[] data = { "e1", "e2", "e3", "e4", "e5"};
82 |
83 | final AsyncGenerator it = AsyncGeneratorQueue.of( queue, q -> {
84 | for( String value: data ) {
85 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
86 | }
87 | });
88 | List iterationResult = it.stream().collect(Collectors.toList());
89 | System.out.println( "Finished iteration " + iterationResult);
90 |
91 | List forEachResult = new ArrayList<>();
92 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
93 | System.out.println( "Finished forEach");
94 | }).join();
95 |
96 | assertEquals( data.length, iterationResult.size() );
97 | assertIterableEquals( Arrays.asList(data), iterationResult );
98 | assertEquals( 0, forEachResult.size() );
99 | }
100 |
101 | @Test
102 | public void asyncGeneratorWithResultStreamTest() throws Exception {
103 |
104 | final BlockingQueue> queue = new LinkedBlockingQueue<>();
105 |
106 | final String[] data = { "e1", "e2", "e3", "e4", "e5"};
107 |
108 | final AsyncGenerator.WithResult it = new AsyncGenerator.WithResult<>( new AsyncGeneratorQueue.Generator<>(queue) );
109 |
110 | commonPool().execute( () -> {
111 | try {
112 | for( String value: data ) {
113 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
114 | }
115 | }
116 | catch( Throwable ex ) {
117 | CompletableFuture error = new CompletableFuture<>();
118 | error.completeExceptionally(ex);
119 | queue.add( AsyncGenerator.Data.of(error));
120 | }
121 | finally {
122 | queue.add(AsyncGenerator.Data.done( "END"));
123 | }
124 |
125 | });
126 |
127 | List iterationResult = it.stream().collect(Collectors.toList());
128 | System.out.println( "Finished iteration " + iterationResult);
129 |
130 | List forEachResult = new ArrayList<>();
131 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
132 | System.out.println( "Finished forEach");
133 | }).join();
134 |
135 |
136 | assertTrue( it.resultValue().isPresent() );
137 | assertEquals( "END", it.resultValue().get() );
138 | assertEquals( data.length, iterationResult.size() );
139 | assertIterableEquals( Arrays.asList(data), iterationResult );
140 | assertEquals( 0, forEachResult.size() );
141 | }
142 |
143 | @Test
144 | public void asyncGeneratorCancelTest() throws Exception {
145 |
146 | final BlockingQueue> queue = new LinkedBlockingQueue<>();
147 |
148 | final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
149 |
150 | final var it = new AsyncGenerator.WithResult<>( new AsyncGeneratorQueue.Generator<>(queue) );
151 |
152 | var executor = Executors.newFixedThreadPool(10);
153 |
154 | executor.execute( () -> {
155 | try {
156 | for( String value: data ) {
157 | Thread.sleep( 1000 );
158 | queue.add(AsyncGenerator.Data.of(completedFuture(value)));
159 | }
160 | queue.add(AsyncGenerator.Data.done( "END"));
161 | }
162 | catch( Throwable ex ) {
163 | CompletableFuture error = new CompletableFuture<>();
164 | error.completeExceptionally(ex);
165 | queue.add( AsyncGenerator.Data.error(ex));
166 | }
167 |
168 | });
169 |
170 | var forEachResult = new ArrayList();
171 |
172 | executor.execute( () -> {
173 | try {
174 | Thread.sleep(3000);
175 | it.cancel( true );
176 | } catch (InterruptedException e) {
177 | throw new RuntimeException(e);
178 | }
179 | });
180 |
181 | var futureResult = it.forEachAsync( value -> {
182 | System.out.println( value );
183 | forEachResult.add(value);
184 | });
185 |
186 | var result = futureResult.get( 10, TimeUnit.SECONDS);
187 |
188 | assertNotNull( result );
189 | assertEquals( CANCELLED, result );
190 | assertTrue( forEachResult.size() < data.size() );
191 |
192 |
193 | }
194 |
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | org.bsc.async
6 | async-generator
7 | 4.0.0-beta2
8 | jar
9 | a Java version of Javascript async generator
10 |
11 | async-generator
12 | https://github.com/bsorrentino/java-async-generator
13 |
14 |
15 |
16 | MIT
17 | https://opensource.org/license/mit
18 |
19 |
20 |
21 |
22 | bsorrentino
23 | Bartolomeo Sorrentino
24 | bartolomeo.sorrentino@gmail.com
25 |
26 |
27 |
28 | scm:git:https://github.com/bsorrentino/java-async-generator.git
29 | scm:git:https://github.com/bsorrentino/java-async-generator.git
30 | https://github.com/bsorrentino/java-async-generator
31 | HEAD
32 |
33 |
34 | github
35 | https://github.com/bsorrentino/java-async-generator/issues
36 |
37 |
38 |
52 |
53 |
54 | UTF-8
55 | 17
56 | 17
57 |
58 |
59 |
60 |
61 |
62 | org.junit
63 | junit-bom
64 | 5.10.2
65 | pom
66 | import
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | org.junit.jupiter
75 | junit-jupiter
76 | test
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | org.apache.maven.plugins
86 | maven-javadoc-plugin
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | org.apache.maven.plugins
98 | maven-javadoc-plugin
99 | 3.11.1
100 |
101 | public
102 | false
103 |
104 | **/module-info.java
105 |
106 |
107 |
108 |
109 |
110 | org.apache.maven.plugins
111 | maven-site-plugin
112 | 4.0.0-M13
113 |
114 |
115 |
116 | org.apache.maven.plugins
117 | maven-jar-plugin
118 | 3.3.0
119 |
120 |
121 |
122 | se.bjurr.gitchangelog
123 | git-changelog-maven-plugin
124 | 1.89
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | org.apache.maven.plugins
133 | maven-surefire-plugin
134 | 3.2.5
135 |
136 |
137 |
138 | java.util.logging.config.file
139 | src/test/resources/logging.properties
140 |
141 |
142 |
143 |
144 |
145 |
146 | se.bjurr.gitchangelog
147 | git-changelog-maven-plugin
148 | false
149 |
150 |
151 | changelog.json
152 | CHANGELOG.md
153 |
154 |
155 |
156 |
157 | org.sonatype.central
158 | central-publishing-maven-plugin
159 | 0.8.0
160 | true
161 |
162 | sonatype-central
163 | true
164 | published
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | release
175 |
176 |
177 |
178 | org.apache.maven.plugins
179 | maven-enforcer-plugin
180 |
181 |
182 | enforce-no-snapshots
183 |
184 | enforce
185 |
186 | verify
187 |
188 |
189 |
190 | No Snapshots Allowed!
191 |
192 |
193 | true
194 |
195 |
196 |
197 |
198 |
203 |
204 | org.apache.maven.plugins
205 | maven-gpg-plugin
206 | 3.2.4
207 |
208 |
209 | sign-artifacts
210 | verify
211 |
212 | sign
213 |
214 |
215 |
216 |
217 | bartolomeo.sorrentino@gmail.com
218 |
219 | --pinentry-mode
220 | loopback
221 |
222 |
223 |
224 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/src/test/java/org/bsc/async/AsyncGeneratorTest.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.concurrent.*;
8 | import java.util.stream.Collectors;
9 |
10 | import static java.util.Arrays.asList;
11 | import static org.junit.jupiter.api.Assertions.*;
12 |
13 | public class AsyncGeneratorTest {
14 |
15 |
16 | @Test
17 | public void asyncGeneratorForEachTest() throws Exception {
18 | final List data = List.of( "e1", "e2", "e3", "e4", "e5" );
19 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
20 |
21 | List forEachResult = new ArrayList<>();
22 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
23 | System.out.println( "Finished forEach");
24 | }).join();
25 |
26 | List iterationResult = new ArrayList<>();
27 | for (String i : it) {
28 | iterationResult.add(i);
29 | System.out.println(i);
30 | }
31 | System.out.println( "Finished iteration");
32 |
33 | assertEquals( data.size(), forEachResult.size() );
34 | assertIterableEquals( data, forEachResult );
35 | assertEquals( 0, iterationResult.size() );
36 | }
37 |
38 | @Test
39 | public void asyncGeneratorForEachCancelTest() throws Exception {
40 |
41 | final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
42 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
43 | final var cancellableIt = new AsyncGenerator.WithResult<>(it);
44 |
45 | CompletableFuture.runAsync( () -> {
46 | try {
47 | Thread.sleep( 2000 );
48 | System.out.printf( "cancellation invoked on thread[%s]\n", Thread.currentThread().getName());
49 | cancellableIt.cancel(true);
50 | } catch (InterruptedException e) {
51 | throw new RuntimeException(e);
52 | }
53 | });
54 |
55 | List forEachResult = new ArrayList<>();
56 | var futureResult = cancellableIt.forEachAsync( value -> {
57 | try {
58 | System.out.printf( "adding element: %s on thread[%s]\n", value, Thread.currentThread().getName());
59 | Thread.sleep( 500 );
60 | forEachResult.add(value);
61 | System.out.printf( "added element: %s\n", value);
62 | } catch (InterruptedException e) {
63 | System.err.printf("interrupted on : %s\n", value );
64 | Thread.currentThread().interrupt();
65 | throw new CompletionException(e);
66 | }
67 | } ).exceptionally( throwable -> {
68 | assertInstanceOf( InterruptedException.class, throwable.getCause());
69 | return AsyncGenerator.Cancellable.CANCELLED;
70 | });
71 |
72 | var result = futureResult.get( 5, TimeUnit.SECONDS);
73 |
74 | assertNotNull( result );
75 | assertEquals(AsyncGenerator.Cancellable.CANCELLED, result );
76 | assertEquals( 3, forEachResult.size() );
77 | assertIterableEquals( data.subList(0,3), forEachResult );
78 |
79 | }
80 |
81 | @Test
82 | public void asyncGeneratorIteratorCancelTest() throws Exception {
83 |
84 | final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
85 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
86 | final var cancellableIt = new AsyncGenerator.WithResult<>(it);
87 |
88 | CompletableFuture.runAsync( () -> {
89 | try {
90 | Thread.sleep( 1000 );
91 | System.out.printf( "cancellation invoked on thread[%s]\n", Thread.currentThread().getName());
92 | cancellableIt.cancel(true);
93 | } catch (InterruptedException e) {
94 | throw new RuntimeException(e);
95 | }
96 | });
97 |
98 | final var iteratorResult = new ArrayList();
99 |
100 | for (String value : cancellableIt) {
101 | try {
102 | System.out.printf( "adding element: %s on thread[%s]\n", value, Thread.currentThread().getName());
103 | Thread.sleep( 500 );
104 | iteratorResult.add(value);
105 | System.out.printf( "added element: %s\n", value);
106 | } catch (InterruptedException e) {
107 | System.err.printf("interrupted on : %s\n", value );
108 | Thread.currentThread().interrupt();
109 | throw new CompletionException(e);
110 | }
111 | }
112 | assertNotEquals( data.size(), iteratorResult.size() );
113 |
114 | }
115 |
116 |
117 | @Test
118 | public void asyncGeneratorMapTest() throws Exception {
119 | final List data = List.of( "a1", "b2", "c3", "d4", "e1" );
120 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
121 |
122 | var forEachResult = it.map( s -> s + "0" )
123 | .reduceAsync( new ArrayList<>(), (result, v) -> {
124 | System.out.println( "add element: " + v);
125 | result.add(v);
126 | return result;
127 | } ).join();
128 |
129 | System.out.println( "Finished iteration");
130 |
131 | assertEquals( data.size(), forEachResult.size() );
132 | assertIterableEquals( asList( "a10", "b20", "c30", "d40", "e10" ), forEachResult );
133 | }
134 |
135 | @Test
136 | public void asyncGeneratorFlatMapTest() throws Exception {
137 | final var data = List.of( 1, 2, 3, 4, 5 );
138 |
139 | final AsyncGenerator it = AsyncGenerator.from(data.iterator())
140 | .flatMap( index -> Task.of( index, 500 ) );
141 |
142 | List forEachResult = new ArrayList<>();
143 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
144 | System.out.println( "Finished forEach " + t);
145 | }).join();
146 |
147 | List iterationResult = new ArrayList<>();
148 | for (String i : it) {
149 | iterationResult.add(i);
150 | System.out.println(i);
151 | }
152 | System.out.println( "Finished iteration");
153 |
154 |
155 | assertEquals( data.size(), forEachResult.size() );
156 | List expected = data.stream().map( index -> "e"+index).collect(Collectors.toList());
157 | assertIterableEquals( expected, forEachResult );
158 | assertEquals( 0, iterationResult.size() );
159 | }
160 |
161 | @Test
162 | public void asyncGeneratorIteratorTest() throws Exception {
163 |
164 | final var data = List.of( "e1", "e2", "e3", "e4", "e5");
165 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
166 |
167 | List iterationResult = new ArrayList<>();
168 | for (String i : it) {
169 | iterationResult.add(i);
170 | System.out.println(i);
171 | }
172 | System.out.println( "Finished iteration " + iterationResult);
173 |
174 | List forEachResult = new ArrayList<>();
175 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
176 | System.out.println( "Finished forEach");
177 | }).join();
178 |
179 | assertEquals( data.size(), iterationResult.size() );
180 | assertIterableEquals( data, iterationResult );
181 | assertEquals( 0, forEachResult.size() );
182 | }
183 |
184 | @Test
185 | public void asyncGeneratorStreamTest() throws Exception {
186 |
187 | final var data = List.of( "e1", "e2", "e3", "e4", "e5");
188 | final AsyncGenerator it = AsyncGenerator.from(data.iterator());
189 | List iterationResult = it.stream().collect(Collectors.toList());
190 | System.out.println( "Finished iteration " + iterationResult);
191 |
192 | List forEachResult = new ArrayList<>();
193 | it.forEachAsync( forEachResult::add ).thenAccept( t -> {
194 | System.out.println( "Finished forEach");
195 | }).join();
196 |
197 | assertEquals( data.size(), iterationResult.size() );
198 | assertIterableEquals(data, iterationResult );
199 | assertEquals( 0, forEachResult.size() );
200 | }
201 |
202 | static class NestedAsyncGenerator extends AsyncGenerator.Base {
203 | int index = -1;
204 | final List data = asList( "e1", "e2", "e3", null, "e4", "e5", "e6", "e7");
205 | final List nestedData = asList( "n1", "n2", "n3", "n4", "n5");
206 |
207 | @Override
208 | public Data next() {
209 | ++index;
210 | if( index >= data.size() ) {
211 | index = -1;
212 | return Data.done( data.size()-1 );
213 | }
214 | if( index == 3) {
215 | return Data.composeWith(
216 | AsyncGenerator.from(nestedData.iterator()),
217 | (v) -> {
218 | System.out.println( "Nested done ");
219 | assertNull(v);
220 | } );
221 | }
222 |
223 | return Data.of( data.get( index ) );
224 | }
225 | }
226 |
227 |
228 |
229 |
230 | @Test
231 | public void asyncEmbedGeneratorTest() throws Exception {
232 | final List expected = List.of( "e1", "e2", "e3", "n1", "n2", "n3", "n4", "n5", "e4", "e5", "e6", "e7");
233 | AsyncGenerator.WithEmbed it = new AsyncGenerator.WithEmbed<>(new NestedAsyncGenerator());
234 |
235 | List forEachResult = new ArrayList<>();
236 | it.forEachAsync( forEachResult::add )
237 | .thenAccept( result -> {
238 | assertEquals( 7, result );
239 | System.out.println( "Finished forEach" );
240 | })
241 | .join();
242 |
243 | assertEquals( 12, forEachResult.size() );
244 | assertIterableEquals( expected, forEachResult );
245 |
246 | List iterationResult = new ArrayList<>();
247 | for (String i : it) {
248 | iterationResult.add(i);
249 | }
250 |
251 | System.out.println( "Finished Iterator");
252 | assertEquals( 12, iterationResult.size() );
253 | assertIterableEquals( expected, iterationResult );
254 |
255 | forEachResult.clear();
256 | it.forEachAsync( forEachResult::add )
257 | .thenAccept( result -> {
258 | assertEquals( 7, result );
259 | System.out.println( "Finished forEach" );
260 | })
261 | .join();
262 |
263 | assertEquals( 12, forEachResult.size() );
264 | assertIterableEquals( expected, forEachResult );
265 | }
266 |
267 | @Test
268 | public void asyncEmbedGeneratorWithResultTest() throws Exception {
269 | final List expected = asList( "e1", "e2", "e3", "n1", "n2", "n3", "n4", "n5", "e4", "e5", "e6", "e7");
270 | AsyncGenerator.WithEmbed it = new AsyncGenerator.WithEmbed<>(new NestedAsyncGenerator(), result -> {
271 | System.out.println( "generator done " );
272 | assertNotNull( result );
273 | assertEquals( 7, result );
274 |
275 | });
276 |
277 | List forEachResult = new ArrayList<>();
278 | it.forEachAsync( forEachResult::add )
279 | .thenAccept( result -> {
280 | assertEquals( 7, result );
281 | System.out.println( "Finished forEach" );
282 | })
283 | .join();
284 |
285 | assertEquals( 12, forEachResult.size() );
286 | assertIterableEquals( expected, forEachResult );
287 | assertEquals( 2, it.resultValues().size() );
288 | Object resultValue = it.resultValues().getFirst().resultValue();
289 | assertNotNull( resultValue );
290 | assertEquals( 7, resultValue );
291 | assertNull( it.resultValues().getLast().resultValue() );
292 |
293 | List iterationResult = new ArrayList<>();
294 | for (String i : it) {
295 | iterationResult.add(i);
296 | }
297 | System.out.println( "Finished Iterator");
298 | assertEquals( 2, it.resultValues().size() );
299 | resultValue = it.resultValues().getFirst().resultValue();
300 | assertNotNull( resultValue );
301 | assertEquals( 7, resultValue );
302 | assertNull( it.resultValues().getLast().resultValue() );
303 |
304 |
305 | assertEquals( 12, iterationResult.size() );
306 | assertIterableEquals( expected, iterationResult );
307 |
308 | forEachResult.clear();
309 | it.forEachAsync( forEachResult::add )
310 | .thenAccept( result -> {
311 | assertEquals( 7, result );
312 | System.out.println( "Finished forEach" );
313 | })
314 | .join();
315 |
316 | assertEquals( 12, forEachResult.size() );
317 | assertIterableEquals( expected, forEachResult );
318 | assertEquals( 2, it.resultValues().size() );
319 | resultValue = it.resultValues().getFirst().resultValue();
320 | assertNotNull( resultValue );
321 | assertEquals( 7, resultValue );
322 | assertNull( it.resultValues().getLast().resultValue());
323 |
324 | }
325 |
326 | @Test
327 | public void asyncEmbedGeneratorWithResultCancelTest() throws Exception {
328 |
329 | AsyncGenerator.WithEmbed it = new AsyncGenerator.WithEmbed<>(new NestedAsyncGenerator(), result -> {
330 | System.out.println( "generator done " );
331 | assertNotNull( result );
332 | assertEquals( 7, result );
333 |
334 | });
335 |
336 | CompletableFuture.runAsync( () -> {
337 | try {
338 | Thread.sleep(2000);
339 | var cancelled = it.cancel( false );
340 | assertTrue( cancelled );
341 | } catch (InterruptedException e) {
342 | throw new RuntimeException(e);
343 | }
344 | });
345 |
346 | List forEachResult = new ArrayList<>();
347 | it.forEachAsync( value -> {
348 | try {
349 | Thread.sleep(200);
350 | forEachResult.add( value );
351 |
352 | } catch (InterruptedException e) {
353 | throw new RuntimeException(e);
354 | }
355 | } )
356 | .thenAccept( result -> {
357 | assertEquals( 7, result );
358 | System.out.println( "Finished forEach" );
359 | })
360 | ;
361 |
362 | assertTrue( it.isCancelled() , "generator should be cancelled");
363 | assertTrue( forEachResult.size() < 12, "result should be partial" );
364 | assertEquals( 1, it.resultValues().size() ); // cancelled on second iterator
365 |
366 | }
367 |
368 | static class AsyncGeneratorWithResult extends AsyncGenerator.Base {
369 | final List elements;
370 | int index = -1;
371 |
372 | AsyncGeneratorWithResult( List elements ) {
373 | this.elements = elements;
374 | }
375 |
376 | @Override
377 | public Data next() {
378 | ++index;
379 | if( index >= elements.size() ) {
380 | index = -1;
381 | return Data.done( elements.size() );
382 | }
383 | return Data.of( elements.get( index ) );
384 | }
385 |
386 | }
387 | @Test
388 | public void asyncGeneratorWithResultTest() throws Exception {
389 | var generator = new AsyncGeneratorWithResult(
390 | List.of( "e1", "e2", "e3", "n1", "n2", "n3", "n4", "n5", "e4", "e5", "e6", "e7"));
391 |
392 | AsyncGenerator it = new AsyncGenerator.WithResult<>(generator);
393 |
394 | it.stream().forEach( System.out::print );
395 | System.out.println();
396 |
397 | assertTrue( AsyncGenerator.resultValue(it).isPresent() );
398 | assertEquals( 12, AsyncGenerator.resultValue(it).get() );
399 |
400 | for( var element : it ) {
401 | System.out.print( element );
402 | }
403 |
404 | assertTrue( AsyncGenerator.resultValue(it).isPresent() );
405 | assertEquals( 12, AsyncGenerator.resultValue(it).get() );
406 | }
407 | }
408 |
--------------------------------------------------------------------------------
/src/main/java/org/bsc/async/AsyncGenerator.java:
--------------------------------------------------------------------------------
1 | package org.bsc.async;
2 |
3 | import org.bsc.async.internal.UnmodifiableDeque;
4 |
5 | import java.util.*;
6 | import java.util.concurrent.*;
7 | import java.util.concurrent.atomic.AtomicBoolean;
8 | import java.util.function.BiFunction;
9 | import java.util.function.Consumer;
10 | import java.util.function.Function;
11 | import java.util.stream.Stream;
12 | import java.util.stream.StreamSupport;
13 |
14 | import static java.lang.String.format;
15 | import static java.util.Objects.requireNonNull;
16 | import static java.util.Optional.ofNullable;
17 | import static java.util.concurrent.CompletableFuture.completedFuture;
18 |
19 | /**
20 | * An asynchronous generator interface that allows generating asynchronous elements.
21 | *
22 | * @param the type of elements. The generator will emit {@link java.util.concurrent.CompletableFuture CompletableFutures<E>} elements
23 | */
24 | public interface AsyncGenerator extends Iterable {
25 |
26 | interface HasResultValue {
27 |
28 | Optional resultValue();
29 | }
30 |
31 | interface IsCancellable {
32 | Object CANCELLED = new Object() {
33 | @Override
34 | public String toString() {
35 | return "CANCELLED";
36 | }
37 | };
38 |
39 | /**
40 | * Checks if the asynchronous generation has been cancelled.
41 | *
42 | * The default implementation always returns {@code false}.
43 | * Implementations that support cancellation should override this method.
44 | *
45 | * @return {@code true} if the generator has been cancelled, {@code false} otherwise.
46 | */
47 | boolean isCancelled();
48 |
49 | /**
50 | * method that request to cancel generator
51 | */
52 | boolean cancel(boolean mayInterruptIfRunning);
53 |
54 |
55 | }
56 |
57 | interface Cancellable extends AsyncGenerator, IsCancellable {
58 |
59 | }
60 |
61 | static Optional resultValue(AsyncGenerator> generator) {
62 | if (generator instanceof HasResultValue withResult) {
63 | return withResult.resultValue();
64 | }
65 | return Optional.empty();
66 | }
67 |
68 | static Optional resultValue(Iterator> iterator) {
69 | if (iterator instanceof HasResultValue withResult) {
70 | return withResult.resultValue();
71 | }
72 | return Optional.empty();
73 | }
74 |
75 | abstract class Base implements AsyncGenerator {
76 |
77 | private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable ->
78 | new Thread(runnable, format("AsyncGenerator[%d]", hashCode())));
79 |
80 | @Override
81 | public Executor executor() {
82 | return executor;
83 | }
84 |
85 | }
86 |
87 | abstract class BaseCancellable extends Base implements Cancellable {
88 |
89 | private final AtomicBoolean cancelled = new AtomicBoolean(false);
90 |
91 | @Override
92 | public boolean isCancelled() {
93 | return cancelled.get();
94 | }
95 |
96 | @Override
97 | public boolean cancel(boolean mayInterruptIfRunning) {
98 | if (cancelled.compareAndSet(false, true)) {
99 | if (executor() instanceof ExecutorService service) {
100 | if (mayInterruptIfRunning && !service.isShutdown() && !service.isTerminated()) {
101 | service.shutdownNow();
102 | }
103 | }
104 | return true;
105 | }
106 | return false;
107 | }
108 |
109 | }
110 |
111 | /**
112 | * An asynchronous generator decorator that allows retrieving the result value of the asynchronous operation, if any.
113 | *
114 | * @param the type of elements in the generator
115 | */
116 | class WithResult extends BaseCancellable implements HasResultValue {
117 |
118 | protected final AsyncGenerator delegate;
119 | private Object resultValue;
120 |
121 | public WithResult(AsyncGenerator delegate) {
122 | this.delegate = delegate;
123 | }
124 |
125 | public AsyncGenerator delegate() {
126 | return delegate;
127 | }
128 |
129 | @Override
130 | public Executor executor() {
131 | return delegate.executor();
132 | }
133 |
134 | /**
135 | * Retrieves the result value of the generator, if any.
136 | *
137 | * @return an {@link Optional} containing the result value if present, or an empty Optional if not
138 | */
139 | public Optional resultValue() {
140 | return ofNullable(resultValue);
141 | }
142 |
143 | @Override
144 | public Data next() {
145 | final Data result = (isCancelled()) ? Data.done(CANCELLED) : delegate.next();
146 |
147 | if (result.isDone()) {
148 | resultValue = result.resultValue();
149 | }
150 | return result;
151 | }
152 |
153 | @Override
154 | public boolean cancel(boolean mayInterruptIfRunning) {
155 | if( super.cancel( mayInterruptIfRunning ) ) {
156 |
157 | if (delegate instanceof IsCancellable isCancellable) {
158 | return isCancellable.cancel(mayInterruptIfRunning);
159 | } else if (mayInterruptIfRunning) {
160 | if (delegate.executor() instanceof ExecutorService service) {
161 | if (!(service.isShutdown() || service.isTerminated())) {
162 | service.shutdownNow();
163 | return true;
164 | }
165 | }
166 | }
167 | }
168 | return false;
169 | }
170 | }
171 |
172 | /**
173 | * An asynchronous generator decorator that allows to generators composition embedding other generators.
174 | *
175 | * @param the type of elements in the generator
176 | */
177 | class WithEmbed extends BaseCancellable implements HasResultValue {
178 | protected final Deque> generatorsStack = new ArrayDeque<>(2);
179 | private final Deque> returnValueStack = new ArrayDeque<>(2);
180 |
181 | public WithEmbed(AsyncGenerator delegate, EmbedCompletionHandler onGeneratorDoneWithResult) {
182 | generatorsStack.push(new Embed<>(delegate, onGeneratorDoneWithResult));
183 | }
184 |
185 | public WithEmbed(AsyncGenerator delegate) {
186 | this(delegate, null);
187 | }
188 |
189 | @Override
190 | public final Executor executor() {
191 | if (generatorsStack.isEmpty()) {
192 | throw new IllegalStateException("no generator found!");
193 | }
194 | return generatorsStack.peek().generator.executor();
195 | }
196 |
197 | public Deque> resultValues() {
198 | return new UnmodifiableDeque<>(returnValueStack);
199 | }
200 |
201 | public Optional resultValue() {
202 | return ofNullable(returnValueStack.peek())
203 | .map(Data::resultValue);
204 | }
205 |
206 | private void clearPreviousReturnsValuesIfAny() {
207 | // Check if the return values are which ones from previous run
208 | if (returnValueStack.size() > 1 && returnValueStack.size() == generatorsStack.size()) {
209 | returnValueStack.clear();
210 | }
211 | }
212 |
213 | protected boolean isLastGenerator() {
214 | return generatorsStack.size() == 1;
215 | }
216 |
217 | @Override
218 | public Data next() {
219 | if (generatorsStack.isEmpty()) { // GUARD
220 | throw new IllegalStateException("no generator found!");
221 | }
222 | if( isCancelled() ) {
223 | return Data.done(CANCELLED);
224 | }
225 |
226 | final Embed embed = requireNonNull(generatorsStack.peek(), "embed generator cannot be null");
227 |
228 | final Data result = embed.generator.next();
229 |
230 |
231 | if (result.isDone()) {
232 | clearPreviousReturnsValuesIfAny();
233 | returnValueStack.push(result);
234 | if (embed.onCompletion != null /* && result.resultValue != null */) {
235 | try {
236 | embed.onCompletion.accept(result.resultValue());
237 | } catch (Exception e) {
238 | return Data.error(e);
239 | }
240 | }
241 | if (isLastGenerator()) {
242 | return result;
243 | }
244 | generatorsStack.pop();
245 | return next();
246 | }
247 | if (result.embed() != null) {
248 | if (generatorsStack.size() >= 2) {
249 | return Data.error(new UnsupportedOperationException("Currently recursive nested generators are not supported!"));
250 | }
251 | generatorsStack.push(result.embed());
252 | return next();
253 | }
254 |
255 | return result;
256 | }
257 |
258 | @Override
259 | public boolean cancel(boolean mayInterruptIfRunning) {
260 | if( super.cancel(mayInterruptIfRunning) ) {
261 | for (var embed : generatorsStack) {
262 | if (embed.generator instanceof Cancellable> isCancellable) {
263 | isCancellable.cancel(mayInterruptIfRunning);
264 | }
265 | }
266 | return true;
267 | }
268 | return false;
269 | }
270 | }
271 |
272 | @FunctionalInterface
273 | interface EmbedCompletionHandler {
274 | void accept(Object t) throws Exception;
275 | }
276 |
277 | class Embed implements HasResultValue {
278 | final AsyncGenerator generator;
279 | final EmbedCompletionHandler onCompletion;
280 |
281 | public Embed(AsyncGenerator generator, EmbedCompletionHandler onCompletion) {
282 | requireNonNull(generator, "generator cannot be null");
283 | this.generator = generator;
284 | this.onCompletion = onCompletion;
285 | }
286 |
287 | @Override
288 | public Optional resultValue() {
289 | return AsyncGenerator.resultValue(generator);
290 | }
291 |
292 | ;
293 | }
294 |
295 | /**
296 | * Represents a data element in the AsyncGenerator.
297 | *
298 | * @param the type of the data element
299 | */
300 | record Data(
301 | CompletableFuture future,
302 | AsyncGenerator.Embed embed,
303 | Object resultValue) {
304 |
305 | public boolean isDone() {
306 | return (future == null && embed == null);
307 | }
308 |
309 | public boolean isError() {
310 | return future != null && future.isCompletedExceptionally();
311 | }
312 |
313 | public static Data of(CompletableFuture future) {
314 | return new Data<>(requireNonNull(future, "future task cannot be null"), null, null);
315 | }
316 |
317 | public static Data of(E data) {
318 | return new Data<>(completedFuture(data), null, null);
319 | }
320 |
321 | public static Data composeWith(AsyncGenerator generator, AsyncGenerator.EmbedCompletionHandler onCompletion) {
322 | return new Data<>(null, new AsyncGenerator.Embed<>(generator, onCompletion), null);
323 | }
324 |
325 | public static Data done() {
326 | return new Data<>(null, null, null);
327 | }
328 |
329 | public static Data done(Object resultValue) {
330 | return new Data<>(null, null, resultValue);
331 | }
332 |
333 | public static Data error(Throwable exception) {
334 | return Data.of(CompletableFuture.failedFuture(exception));
335 | }
336 |
337 | }
338 |
339 | /**
340 | * Retrieves the next asynchronous element.
341 | *
342 | * @return the next element from the generator
343 | */
344 | Data next();
345 |
346 |
347 | Executor executor();
348 |
349 | /**
350 | * Maps the elements of this generator to a new asynchronous generator.
351 | *
352 | * @param mapFunction the function to map elements to a new asynchronous counterpart
353 | * @param the type of elements in the new generator
354 | * @return a generator with mapped elements
355 | */
356 | default AsyncGenerator map(Function mapFunction) {
357 | return new Mapper<>(this, mapFunction);
358 | }
359 |
360 | /**
361 | * Maps the elements of this generator to a new asynchronous generator, and flattens the resulting nested generators.
362 | *
363 | * @param mapFunction the function to map elements to a new asynchronous counterpart
364 | * @param the type of elements in the new generator
365 | * @return a generator with mapped and flattened elements
366 | */
367 | default AsyncGenerator flatMap(Function> mapFunction) {
368 | return new FlatMapper<>(this, mapFunction);
369 | }
370 |
371 | private CompletableFuture forEachSync(Consumer consumer) {
372 | final var next = next();
373 | if (next.isDone()) {
374 | return completedFuture(next.resultValue());
375 | }
376 | if (next.embed() != null) {
377 | return next.embed().generator.forEachSync(consumer)
378 | .thenCompose(v -> forEachSync(consumer))
379 | ;
380 | } else {
381 | return next.future()
382 | .thenApply(v -> {
383 | consumer.accept(v);
384 | return null;
385 | })
386 | .thenCompose(v -> forEachSync(consumer))
387 | ;
388 | }
389 |
390 | }
391 |
392 | /**
393 | * Asynchronously iterates over the elements of the AsyncGenerator and applies the given consumer to each element.
394 | *
395 | * @param consumer the consumer function to be applied to each element
396 | * @return a CompletableFuture representing the completion of the iteration process.
397 | */
398 | default CompletableFuture forEachAsync(Consumer consumer) {
399 | return CompletableFuture.supplyAsync(() -> forEachSync(consumer), executor()).join();
400 | }
401 |
402 | default CompletableFuture reduce(R result, BiFunction reducer) {
403 | final var next = next();
404 | if (next.isDone()) {
405 | return completedFuture(result);
406 | }
407 | return next.future()
408 | .thenApply(v -> reducer.apply(result, v))
409 | .thenCompose(v -> reduce(result, reducer))
410 | ;
411 |
412 | }
413 |
414 | default CompletableFuture reduceAsync(R result, BiFunction reducer) {
415 | return CompletableFuture.supplyAsync(() -> reduce(result, reducer), executor()).join();
416 | }
417 |
418 | /**
419 | * Converts the AsyncGenerator to a CompletableFuture.
420 | *
421 | * @return a CompletableFuture representing the completion of the AsyncGenerator
422 | */
423 | default CompletableFuture toCompletableFuture() {
424 | final Data next = next();
425 | if (next.isDone()) {
426 | return completedFuture(next.resultValue());
427 | }
428 | return next.future().thenCompose(v -> toCompletableFuture());
429 | }
430 |
431 | /**
432 | * Returns a sequential Stream with the elements of this AsyncGenerator.
433 | * Each CompletableFuture is resolved and then make available to the stream.
434 | *
435 | * @return a Stream of elements from the AsyncGenerator
436 | */
437 | default Stream stream() {
438 | return StreamSupport.stream(
439 | Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED),
440 | false);
441 | }
442 |
443 | /**
444 | * Returns an iterator over the elements of this AsyncGenerator.
445 | * Each call to `next` retrieves the next "resolved" asynchronous element from the generator.
446 | *
447 | * @return an iterator over the elements of this AsyncGenerator
448 | */
449 | default Iterator iterator() {
450 | return new InternalIterator(this);
451 | }
452 |
453 |
454 | /**
455 | * Returns an empty AsyncGenerator.
456 | *
457 | * @param the type of elements
458 | * @return an empty AsyncGenerator
459 | */
460 | static AsyncGenerator empty() {
461 | return new Base<>() {
462 | @Override
463 | public Data next() {
464 | return Data.done();
465 | }
466 | };
467 | }
468 |
469 | /**
470 | * Collects asynchronous elements from an iterator.
471 | *
472 | * @param the type of elements in the iterator
473 | * @param iterator the iterator containing elements to collect
474 | * @return an AsyncGenerator instance with collected elements
475 | */
476 | static AsyncGenerator from(Iterator iterator) {
477 | return new Base<>() {
478 | @Override
479 | public Data next() {
480 |
481 | if (!iterator.hasNext()) {
482 | return Data.done();
483 | }
484 | return Data.of(completedFuture(iterator.next()));
485 | }
486 | };
487 | }
488 |
489 | }
490 |
491 | class InternalIterator implements Iterator, AsyncGenerator.HasResultValue, AsyncGenerator.IsCancellable {
492 | private final AsyncGenerator delegate;
493 |
494 | //final AtomicReference> currentFetchedData;
495 | private volatile AsyncGenerator.Data currentFetchedData;
496 |
497 | public InternalIterator(AsyncGenerator delegate) {
498 | this.delegate = delegate;
499 | //currentFetchedData = new AtomicReference<>(delegate.next());
500 | currentFetchedData = delegate.next();
501 | }
502 |
503 | @Override
504 | public boolean hasNext() {
505 | if( isCancelled() ) {
506 | return false;
507 | }
508 | //final var value = currentFetchedData.get();
509 | final var value = currentFetchedData;
510 | return value != null && !value.isDone();
511 | }
512 |
513 | @Override
514 | public E next() {
515 | if( isCancelled() ) {
516 | throw new CancellationException("generator is cancelled");
517 | }
518 | //var next = currentFetchedData.get();
519 | var next = currentFetchedData;
520 |
521 | if (next == null || next.isDone()) {
522 | throw new IllegalStateException("no more elements into iterator");
523 | }
524 |
525 | if (!next.isError()) {
526 | //currentFetchedData.set(delegate.next());
527 | currentFetchedData = delegate.next();
528 | }
529 |
530 | return next.future().join();
531 | }
532 |
533 | @Override
534 | public Optional resultValue() {
535 | if (delegate instanceof AsyncGenerator.HasResultValue withResult) {
536 | return withResult.resultValue();
537 | }
538 | return Optional.empty();
539 | }
540 |
541 | @Override
542 | public boolean isCancelled() {
543 | if (delegate instanceof AsyncGenerator.IsCancellable isCancellable) {
544 | return isCancellable.isCancelled();
545 | }
546 | return false;
547 | }
548 |
549 | @Override
550 | public boolean cancel(boolean mayInterruptIfRunning) {
551 | if (delegate instanceof AsyncGenerator.IsCancellable isCancellable) {
552 | return isCancellable.cancel(mayInterruptIfRunning);
553 | }
554 | return false;
555 | }
556 | };
557 |
558 | class Mapper extends AsyncGenerator.BaseCancellable implements AsyncGenerator.HasResultValue {
559 |
560 | protected final AsyncGenerator delegate;
561 | final Function mapFunction;
562 | private Object resultValue;
563 |
564 | protected Mapper(AsyncGenerator delegate, Function mapFunction) {
565 | this.delegate = requireNonNull(delegate, "delegate cannot be null");
566 | this.mapFunction = requireNonNull(mapFunction, "mapFunction cannot be null");
567 | }
568 |
569 | @Override
570 | public final Executor executor() {
571 | return delegate.executor();
572 | }
573 |
574 | /**
575 | * Retrieves the result value of the generator, if any.
576 | *
577 | * @return an {@link Optional} containing the result value if present, or an empty Optional if not
578 | */
579 | public Optional resultValue() {
580 | return ofNullable(resultValue);
581 | }
582 |
583 | @Override
584 | public final Data next() {
585 | if( isCancelled() ) {
586 | throw new CancellationException("generator is cancelled");
587 | }
588 |
589 | final Data next = delegate.next();
590 |
591 | if (next.isDone()) {
592 | resultValue = next.resultValue();
593 | return Data.done(next.resultValue());
594 | }
595 | return Data.of(next.future().thenApply(mapFunction));
596 | }
597 |
598 | @Override
599 | public boolean cancel(boolean mayInterruptIfRunning) {
600 | if( super.cancel( mayInterruptIfRunning ) ) {
601 |
602 | if (delegate instanceof IsCancellable isCancellable) {
603 | return isCancellable.cancel(mayInterruptIfRunning);
604 | } else if (mayInterruptIfRunning) {
605 | if (delegate.executor() instanceof ExecutorService service) {
606 | if (!(service.isShutdown() || service.isTerminated())) {
607 | service.shutdownNow();
608 | return true;
609 | }
610 | }
611 | }
612 | }
613 | return false;
614 | }
615 | }
616 |
617 | class FlatMapper extends AsyncGenerator.BaseCancellable implements AsyncGenerator.HasResultValue {
618 |
619 | protected final AsyncGenerator delegate;
620 | final Function> mapFunction;
621 | private Object resultValue;
622 |
623 | protected FlatMapper(AsyncGenerator delegate, Function> mapFunction) {
624 | this.delegate = requireNonNull(delegate, "delegate cannot be null");
625 | this.mapFunction = requireNonNull(mapFunction, "mapFunction cannot be null");
626 |
627 | }
628 |
629 | @Override
630 | public final Executor executor() {
631 | return delegate.executor();
632 | }
633 |
634 | /**
635 | * Retrieves the result value of the generator, if any.
636 | *
637 | * @return an {@link Optional} containing the result value if present, or an empty Optional if not
638 | */
639 | public Optional resultValue() {
640 | return ofNullable(resultValue);
641 | }
642 |
643 | ;
644 |
645 | @Override
646 | public final Data next() {
647 | if( isCancelled() ) {
648 | throw new CancellationException("generator is cancelled");
649 | }
650 |
651 | final Data next = delegate.next();
652 |
653 | if (next.isDone()) {
654 | resultValue = next.resultValue();
655 | return Data.done(next.resultValue());
656 | }
657 | return Data.of(next.future().thenCompose(mapFunction));
658 | }
659 |
660 | @Override
661 | public boolean cancel(boolean mayInterruptIfRunning) {
662 | if( super.cancel( mayInterruptIfRunning ) ) {
663 |
664 | if (delegate instanceof IsCancellable isCancellable) {
665 | return isCancellable.cancel(mayInterruptIfRunning);
666 | } else if (mayInterruptIfRunning) {
667 | if (delegate.executor() instanceof ExecutorService service) {
668 | if (!(service.isShutdown() || service.isTerminated())) {
669 | service.shutdownNow();
670 | return true;
671 | }
672 | }
673 | }
674 | }
675 | return false;
676 | }
677 | }
678 |
679 |
680 |
681 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 |
5 |
6 |
7 | ## [v4.0.0-beta2](https://github.com/bsorrentino/java-async-generator/releases/tag/v4.0.0-beta2) (2025-10-02)
8 |
9 |
10 |
11 | ### Documentation
12 |
13 | - bump to 4.0.0-beta2 version ([2f8860fb77fc1b4](https://github.com/bsorrentino/java-async-generator/commit/2f8860fb77fc1b45521f1904eb32b8cc9f2de7d2))
14 |
15 | - update cancellation doc ([7e100f9432b5726](https://github.com/bsorrentino/java-async-generator/commit/7e100f9432b572627b55eb074adee21d18ab5585))
16 |
17 | - update cancellation doc ([07c9e389b439c96](https://github.com/bsorrentino/java-async-generator/commit/07c9e389b439c965043884c8e490e336fac2837b))
18 |
19 | - update GEMINI cli default prompt ([bc0aa5bfeebba13](https://github.com/bsorrentino/java-async-generator/commit/bc0aa5bfeebba131ddecbe9cdb166ae7eb3f11f7))
20 |
21 | - refine cancellation guide ([fc689106ec11025](https://github.com/bsorrentino/java-async-generator/commit/fc689106ec110250e745c3520294bfa7408fe0d8))
22 | > work on #2
23 |
24 | - update changeme ([f9fa28ac55169c1](https://github.com/bsorrentino/java-async-generator/commit/f9fa28ac55169c105484f26d385de6f7a1ca1d91))
25 |
26 |
27 | ### Refactor
28 |
29 | - return CANCELLED result if cancellation is detected in next() method ([4f5c479031a3169](https://github.com/bsorrentino/java-async-generator/commit/4f5c479031a31691cd6f22f9a1255b23f2e97db5))
30 | > work on #2
31 |
32 | - **AsyncGenerator** Refactored cancellation logic across AsyncGenerator subclasses ([1046e25b0004110](https://github.com/bsorrentino/java-async-generator/commit/1046e25b00041100da297afd21784413882eae77))
33 | > - handle cancellation regardless that delegate is cancellable
34 | > work on #2
35 |
36 |
37 | ### ALM
38 |
39 | - bump to 4.0.0-beta2 version ([720ef0ba54250e3](https://github.com/bsorrentino/java-async-generator/commit/720ef0ba54250e3fe5a07291bf73a9843b1031f9))
40 |
41 | - **action** update deploy snapshot github action ([0d324a536bddd74](https://github.com/bsorrentino/java-async-generator/commit/0d324a536bddd741f7e1e913ac990bdbe8a2dacf))
42 |
43 | - bump to 4.0-SNAPSHOT release ([756ba4fb9f4461f](https://github.com/bsorrentino/java-async-generator/commit/756ba4fb9f4461f43974e3b732bc060deff61f00))
44 |
45 | - update deploy script ([4cc8accbbda3812](https://github.com/bsorrentino/java-async-generator/commit/4cc8accbbda3812517defcd5ed880b6e29632a5e))
46 |
47 |
48 | ### Test
49 |
50 | - **AsyncGenerator** Refactored cancellation logic across AsyncGenerator subclasses ([23b9a57ff806dcf](https://github.com/bsorrentino/java-async-generator/commit/23b9a57ff806dcfafae764734a31117b131ab1f6))
51 | > - handle cancellation regardless that delegate is cancellable
52 | > work on [#2](https://github.com/bsorrentino/java-async-generator/issues/2)
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ## [v4.0.0-beta1](https://github.com/bsorrentino/java-async-generator/releases/tag/v4.0.0-beta1) (2025-10-01)
62 |
63 | ### Features
64 |
65 | * use service.shutdownNow() to force thread interruption ([4942910ec9fafcd](https://github.com/bsorrentino/java-async-generator/commit/4942910ec9fafcd9023b1ee990a21b6911aa11d1))
66 |
67 | * Add AbstractCancellableAsyncGenerator class ([2512f981de6ba91](https://github.com/bsorrentino/java-async-generator/commit/2512f981de6ba91d59fef936f1a5dba16ab7f4f4))
68 | > - new abstract class that implements AsyncGenerator.Cancellable interface, providing functionality to keep cancellation state .
69 | > work on #2
70 |
71 | * **AsyncGenerator** Update AsyncGenerator interface for better cancellation support ([731abd0a76e3d2e](https://github.com/bsorrentino/java-async-generator/commit/731abd0a76e3d2ede66d11711910808ba84ebf71))
72 | > - Added Cancellable
73 | > - Updated WithResult and WithEmbed classes to implement these new interfaces for managing cancellation request.
74 | > work on #2
75 |
76 |
77 | ### Bug Fixes
78 |
79 | - **AsyncGenerator** Ensure Data.done() includes resultValue ([c5f59dea24202cc](https://github.com/bsorrentino/java-async-generator/commit/c5f59dea24202cc88ce9e1294f24a463b059a1d3))
80 | > Modify return statements in internal class Mapper and FlatMapper to pass resultValue w.
81 | > work on #2
82 |
83 |
84 | ### Documentation
85 |
86 | - bump to new version 4.0.0-beta1 ([76daba668e13893](https://github.com/bsorrentino/java-async-generator/commit/76daba668e1389343ba46f5e523aa1b46fa48776))
87 |
88 | - update readme ([42d058811c1b28a](https://github.com/bsorrentino/java-async-generator/commit/42d058811c1b28af06c048b269b24199ee7573b9))
89 |
90 | - **ai** prompt used in gemini cli ([fc84ce5aaec8c3f](https://github.com/bsorrentino/java-async-generator/commit/fc84ce5aaec8c3fed7ef1a2ca607809383d0583c))
91 |
92 | - add cancellation document ([668092008616bf4](https://github.com/bsorrentino/java-async-generator/commit/668092008616bf42cbad28e648d70dc20c303bd1))
93 |
94 | - update changeme ([bb10611598b04f2](https://github.com/bsorrentino/java-async-generator/commit/bb10611598b04f2b901ed9c611c2588c3969dab9))
95 |
96 |
97 | ### Refactor
98 |
99 | - **AsyncGenerator** Rename abstract class BaseCancellable to use Cancellable interface instead of IsCancellable ([9b87691850bf9bd](https://github.com/bsorrentino/java-async-generator/commit/9b87691850bf9bd66b411fcf611d7adf1711986b))
100 | > work on #2
101 |
102 | - **AsyncGenerator** extract feature driven IsCancelled interface ([805925b193860e8](https://github.com/bsorrentino/java-async-generator/commit/805925b193860e8c32a919566e8a143b25dd2e41))
103 | > work on #2
104 |
105 | - **AsyncGenerator** make reduce public ([06099f66d0d8a14](https://github.com/bsorrentino/java-async-generator/commit/06099f66d0d8a14740e4c83916933d422145e453))
106 | > work on #2
107 |
108 | - **AsyncGenerator** rename reduceSync to reduce and update references ([56d4e15242918da](https://github.com/bsorrentino/java-async-generator/commit/56d4e15242918da2d03e2467fd57cfb98dfbead4))
109 | > work on #2
110 |
111 | - Merge AsyncGenerator with AsyncGeneratorBase ([13a9c8a4b8d6617](https://github.com/bsorrentino/java-async-generator/commit/13a9c8a4b8d6617a6fa23b529048039458d8283d))
112 | > work on #2
113 |
114 | - **reactive** update with new Cancellable model ([c2c5437928bf1cd](https://github.com/bsorrentino/java-async-generator/commit/c2c5437928bf1cd76337fcc35bb0941487be0460))
115 |
116 | - **AsyncGenerator** Refactor AsyncGenerator class with ExecutorService support to force execution on a single controlled thread ([e9439a1fc9f8ea7](https://github.com/bsorrentino/java-async-generator/commit/e9439a1fc9f8ea7bf7e3aacda89abed3c662e1f5))
117 | > work on #2
118 |
119 | - remove unused AbstractCancellableAsyncGenerator ([57c3e0251b4d54f](https://github.com/bsorrentino/java-async-generator/commit/57c3e0251b4d54ffed086e1f6e4608360efd9750))
120 | > work on #2
121 |
122 | - make FlowGenerator compliant with AsyncGenerator.Cancellable ([2f07f1b4a83f24f](https://github.com/bsorrentino/java-async-generator/commit/2f07f1b4a83f24fa212dc479fbe6865caff7cdf1))
123 | > work on #2
124 |
125 | - **reactive** Refactor GeneratorSubscriber to manage cancellation ([4f6f8b3f468676f](https://github.com/bsorrentino/java-async-generator/commit/4f6f8b3f468676f6966baf213f0e03dac176cac9))
126 | > - adding a private subscription field to hold the Flow.Subscription on which we will invoke cancel() during cancellation request process
127 | > work on #2
128 |
129 | - **AsyncGeneratorQueue** Refactor AsyncGeneratorQueue to manage cancellation ([0e84e4c5a5afaf6](https://github.com/bsorrentino/java-async-generator/commit/0e84e4c5a5afaf6d21eec145d948350090f0609a))
130 | > - keep track of execution thread and perform its interruption on cancellation request
131 | > work on #2
132 |
133 | - rename AsyncGeneratorOperators to AsyncGeneratorBase ([53efcfba852cf51](https://github.com/bsorrentino/java-async-generator/commit/53efcfba852cf519655d75351af2b73474d877c7))
134 | > work on #2
135 |
136 |
137 | ### ALM
138 |
139 | - bump to new version 4.0.0-beta1 ([12b2a80a091728c](https://github.com/bsorrentino/java-async-generator/commit/12b2a80a091728c983cbcaec2082a3085a8979e0))
140 |
141 | - bump to version 4.0-SNAPSHOT ([c81d50f7879c735](https://github.com/bsorrentino/java-async-generator/commit/c81d50f7879c7354f6973b8133e548968edea30f))
142 |
143 | - bump to 3.2-SNAPSHOT version ([e20d846ba5d9c3e](https://github.com/bsorrentino/java-async-generator/commit/e20d846ba5d9c3eacba363663901e1d2ce8cdb11))
144 |
145 | - **settings-template.xml** update xml namespaces ([df2f07ac67f362f](https://github.com/bsorrentino/java-async-generator/commit/df2f07ac67f362f87bfe8cbe8d326afa344d99de))
146 |
147 |
148 | ### Test
149 |
150 | - update unit test add cancellation tests ([c7e9f28e31b9387](https://github.com/bsorrentino/java-async-generator/commit/c7e9f28e31b938797854c0f88a98edf729daa895))
151 |
152 | - add cancellation tests ([cb6b15653a2b860](https://github.com/bsorrentino/java-async-generator/commit/cb6b15653a2b86035b883c025475be942f4aa0c6))
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | ## [v3.2.3](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.2.3) (2025-09-19)
162 |
163 | ### Features
164 |
165 | * **GeneratorPublisher** Update cancellation method implementation ([324a10e2a85f62e](https://github.com/bsorrentino/java-async-generator/commit/324a10e2a85f62eb8820e26d5407148f449ff8b8))
166 | > Replace throw statement with delegate.cancel() call to handle cancellation request on subscription.
167 | > work on #2
168 |
169 | * add cancel method to allow its extensions to implement a cancellation strategy ([dec058e58d25e23](https://github.com/bsorrentino/java-async-generator/commit/dec058e58d25e230a489210b0d0c0be8c22de061))
170 | > work on #2
171 |
172 |
173 |
174 | ### Documentation
175 |
176 | - bump to version 3.2.3 ([11cf718cf09df62](https://github.com/bsorrentino/java-async-generator/commit/11cf718cf09df62ff24eec9901761a633517d598))
177 |
178 | - update changeme ([363a524c24fc79f](https://github.com/bsorrentino/java-async-generator/commit/363a524c24fc79fec0a024e1777a978329e69378))
179 |
180 | - update changeme ([26c0fb3fd9228ff](https://github.com/bsorrentino/java-async-generator/commit/26c0fb3fd9228ffe0a0feea160e60ae4cecb27bf))
181 |
182 |
183 | ### Refactor
184 |
185 | - default implementation of cancel() method raises UnsupportedOperationException ([a46cf8a01937f7c](https://github.com/bsorrentino/java-async-generator/commit/a46cf8a01937f7c128a25480e912bfdac059d85a))
186 | > work on #2
187 |
188 | - **AsyncGeneratorQueue** Removed deprecated method of(Q, Consumer, Executor) from AsyncGeneratorQueue ([6b5e7eec9f42d36](https://github.com/bsorrentino/java-async-generator/commit/6b5e7eec9f42d362b73d0fa1aafbf91d86c8c039))
189 |
190 |
191 | ### ALM
192 |
193 | - bump to version 3.2.3 ([acc241506cdf609](https://github.com/bsorrentino/java-async-generator/commit/acc241506cdf6098b2eb32d782963e773164dfb2))
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | ## [v3.2.2](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.2.2) (2025-07-10)
204 |
205 | ### Features
206 |
207 | * improve support for retrieve generator return value ([df3f83cf75f3d61](https://github.com/bsorrentino/java-async-generator/commit/df3f83cf75f3d61b40494dc862c00c6e92437839))
208 | > - add support of return value to iterator
209 | > - add utility methods for query return value
210 |
211 |
212 |
213 | ### Documentation
214 |
215 | - update changeme ([e8846913e3d1b49](https://github.com/bsorrentino/java-async-generator/commit/e8846913e3d1b49527f6d2a33d51d35dae5c2b91))
216 |
217 |
218 | ### Refactor
219 |
220 | - **deploy** refactor: move to sonatype-central deployment repo ([f7365d58f4e75e2](https://github.com/bsorrentino/java-async-generator/commit/f7365d58f4e75e2bbbefe841c2ef3298247813b6))
221 |
222 | - move to sonatype-central deployment repo ([c43cac491b9fa80](https://github.com/bsorrentino/java-async-generator/commit/c43cac491b9fa801933a209ecfb0200c5d6c34e6))
223 |
224 |
225 | ### ALM
226 |
227 | - bump to version 3.2.2 ([25d0d2d4f9eb641](https://github.com/bsorrentino/java-async-generator/commit/25d0d2d4f9eb6419a86c40dce64a8009251fea38))
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 | ## [v3.2.1](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.2.1) (2025-06-22)
238 |
239 | ### Features
240 |
241 | * add method to create async generator with a default executor ([6a56c5a89d3c8d5](https://github.com/bsorrentino/java-async-generator/commit/6a56c5a89d3c8d52833244d39939f86c1bcfdec3))
242 | > - Added `async()` method as a default implementation that uses the common `ForkJoinPool`.
243 |
244 |
245 |
246 | ### Documentation
247 |
248 | - update README.md ([c0edb346316dc82](https://github.com/bsorrentino/java-async-generator/commit/c0edb346316dc82e63e49d048a7545d56def9213))
249 |
250 | - update changeme ([623e0cda5d251ad](https://github.com/bsorrentino/java-async-generator/commit/623e0cda5d251adb51a55cdfb0346af523c526d9))
251 |
252 |
253 | ### Refactor
254 |
255 | - replace ReentrantReadWriteLock with AtomicReference ([49ae7dd5cbed212](https://github.com/bsorrentino/java-async-generator/commit/49ae7dd5cbed212aec341592783e84803013abc7))
256 | > - Introduced `AtomicReference` to manage the current fetched data, simplifying the locking mechanism and improving performance.
257 | > - Removed unnecessary read and write locks, reducing lock contention and enhancing concurrency.
258 | > resolve #3
259 |
260 |
261 | ### ALM
262 |
263 | - bump to version 3.2.1 ([007965d9fd82b5b](https://github.com/bsorrentino/java-async-generator/commit/007965d9fd82b5b32b7c0776067aaae2c1ce4c1b))
264 |
265 | - update JDK version to Java 17 in deploy-snapshot.yaml action ([539dd9fa2e6799e](https://github.com/bsorrentino/java-async-generator/commit/539dd9fa2e6799e888f9e56176eecbf9b5425753))
266 |
267 | - move to 3.2-SNAPSHOT ([3af1271300a9e96](https://github.com/bsorrentino/java-async-generator/commit/3af1271300a9e960eda3ba134c3daea28d6984ce))
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | ## [v3.2.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.2.0) (2025-05-12)
278 |
279 | ### Features
280 |
281 | * **GeneratorSubscriber** add optional mapping for generator's result ([c123e0e307052bf](https://github.com/bsorrentino/java-async-generator/commit/c123e0e307052bf19fb4b4f274e3102b39ac3a13))
282 | > - Updated the `onComplete` method to use the `mapResult` function when generating the completion data.
283 |
284 | * **FlowGenerator** add parameter to map result from publisher ([e5d7f78165d8089](https://github.com/bsorrentino/java-async-generator/commit/e5d7f78165d8089059515b7cba2a556b29746c54))
285 |
286 | * **AsyncGenerator** add isError() method to handle exceptions in asynchronous data ([1cf6811547d0296](https://github.com/bsorrentino/java-async-generator/commit/1cf6811547d02966ff8d35f121c21c1fa2965151))
287 | > - remove deprecated methods
288 |
289 |
290 |
291 | ### Documentation
292 |
293 | - update changeme ([f8d0b99fdac84d9](https://github.com/bsorrentino/java-async-generator/commit/f8d0b99fdac84d963a741f6039dc6c693035e265))
294 |
295 | - update changeme ([9de329999bbcd8c](https://github.com/bsorrentino/java-async-generator/commit/9de329999bbcd8cdfcf265aaf6b85b93e2f37719))
296 |
297 |
298 | ### Refactor
299 |
300 | - update changelog template ([0acddcf388ee5bb](https://github.com/bsorrentino/java-async-generator/commit/0acddcf388ee5bb858105c8bfccf94c350d328d9))
301 |
302 |
303 | ### ALM
304 |
305 | - bump to 3.2.0 version ([c2cb40e451208a0](https://github.com/bsorrentino/java-async-generator/commit/c2cb40e451208a0eb3dd8e0eaba5af1030e2e709))
306 |
307 | - bump to SNAPSHOT ([659e9eecad7d23c](https://github.com/bsorrentino/java-async-generator/commit/659e9eecad7d23c7a8925e52a3e72318a47f2986))
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | ## [v3.1.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.1.0) (2025-04-16)
318 |
319 | ### Features
320 |
321 | * **AsyncGenerator.java** add interface HasResultValue to support result value retrieval ([fee0822f6cbacd3](https://github.com/bsorrentino/java-async-generator/commit/fee0822f6cbacd32bcd7ae080e28a0c466ba7676))
322 |
323 | * **tests** Added unit test AsyncGeneratorAsyncTest for asynchronous generator processing ([8f15f0bbd5ae681](https://github.com/bsorrentino/java-async-generator/commit/8f15f0bbd5ae681cc661e9f988f02ee802bdfd8c))
324 | > - Removed unused import and disabled the `asyncGenTest` method
325 |
326 |
327 | ### Bug Fixes
328 |
329 | - configure Maven Javadoc Plugin to exclude module-info.java ([d9adb3d9fcc273e](https://github.com/bsorrentino/java-async-generator/commit/d9adb3d9fcc273ec939a920f42dd33a6bae287a1))
330 |
331 |
332 | ### Documentation
333 |
334 | - update javadoc ([78f72b0555519ac](https://github.com/bsorrentino/java-async-generator/commit/78f72b0555519ac5ea738499886476a7e3c2c8e4))
335 |
336 | - update changeme ([3b0314aa86dfce3](https://github.com/bsorrentino/java-async-generator/commit/3b0314aa86dfce3190d8ac3377de8217356a117b))
337 |
338 |
339 | ### Refactor
340 |
341 | - **AsyncGeneratorOperators.java** update async usage ([71aab83bb26c04a](https://github.com/bsorrentino/java-async-generator/commit/71aab83bb26c04a87b6285bd6f730455f61e7529))
342 | > - use async operator thenApplyAsync
343 | > - Removed redundant and unnecessary nested `forEachAsyncNested` and `collectAsyncNested` methods.
344 |
345 | - **AsyncGenerator.java** update deprecation annotations to specify removal ([2f143364b93650e](https://github.com/bsorrentino/java-async-generator/commit/2f143364b93650e2e8210569cd6187b4ba809e1b))
346 | > - Updated `@Deprecated` annotations to include `forRemoval = true` in `collectAsync` methods.
347 |
348 |
349 | ### ALM
350 |
351 | - bump to 3.1.0 version ([edc31a64edbc051](https://github.com/bsorrentino/java-async-generator/commit/edc31a64edbc051103ecad17fbab5b037aed0bd7))
352 |
353 | - bump to new SNAPSHOT ([4ccd38e1441a85d](https://github.com/bsorrentino/java-async-generator/commit/4ccd38e1441a85d4012d9daf24afd1e17dbac80a))
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | ## [v3.0.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v3.0.0) (2025-01-21)
364 |
365 | ### Features
366 |
367 | * **reactive** add reactive stream integration ([9bf82ed82a55f4e](https://github.com/bsorrentino/java-async-generator/commit/9bf82ed82a55f4edafd0da75d1b4be6ee37907d2))
368 | > - AsyncGenerator fromPublisher( Flow.Publisher )
369 | > - Flow.Publisher toPublisher( AsyncGenerator )
370 | > resolve #1
371 |
372 |
373 | ### Bug Fixes
374 |
375 | - **AsyncGenerator** add UnmodifiableDeque import ([47d4b323f0f0306](https://github.com/bsorrentino/java-async-generator/commit/47d4b323f0f0306425f7464c925bbb239433df2e))
376 |
377 |
378 | ### Documentation
379 |
380 | - update readme ([eaa1967766673c3](https://github.com/bsorrentino/java-async-generator/commit/eaa1967766673c3f226e956af199fe9d510f7fa4))
381 |
382 | - add javadoc ([86841005e8dadf8](https://github.com/bsorrentino/java-async-generator/commit/86841005e8dadf840c94410f3b65d909aa767082))
383 | > work on #1
384 |
385 | - update javadocs ([686473afb186e53](https://github.com/bsorrentino/java-async-generator/commit/686473afb186e53c2d05868809f7875d6a3e817e))
386 |
387 | - update changeme ([2de8da8952074d6](https://github.com/bsorrentino/java-async-generator/commit/2de8da8952074d6f050e00f6eb815590aa3eb4c8))
388 |
389 |
390 | ### Refactor
391 |
392 | - **AsyncGeneratorOperators.java** improve asynchronous iteration and collection ([9dd666d47a19627](https://github.com/bsorrentino/java-async-generator/commit/9dd666d47a19627747a310326e7c37d763073031))
393 | > This commit refactors the `AsyncGeneratorOperators` interface to improve the performance of asynchronous iteration and collection operations. The changes include:
394 | > 1. Introducing a private method `forEachAsyncNested` to handle nested asynchronous iterations without spawning new threads.
395 | > 2. Using `supplyAsync` within the `forEachAsync` method to ensure that each iteration can be performed concurrently, enhancing parallel processing capabilities.
396 | > 3. Similarly, refactoring the `collectAsync` and `collectAsyncNested` methods to collect elements asynchronously efficiently.
397 |
398 | - **FlowGenerator** rename package and class ([43958c928c2ae68](https://github.com/bsorrentino/java-async-generator/commit/43958c928c2ae680f1f73b87529a4ef535eb25dc))
399 | > Renamed 'FluxGenerator' to 'FlowGenerator' and updated imports accordingly. This change improves readability and consistency within the project.
400 |
401 | - **maven-javadoc-plugin** update version to 3.11.1 ([ef818c6b26b81fe](https://github.com/bsorrentino/java-async-generator/commit/ef818c6b26b81fe66a2cb416117e60296722e7ff))
402 |
403 |
404 | ### ALM
405 |
406 | - bump to version 3.0.0 ([effd36c53f23915](https://github.com/bsorrentino/java-async-generator/commit/effd36c53f239157646531f6db8c916fe68489e9))
407 |
408 | - move private classes in internal package ([bace2d332d2ea34](https://github.com/bsorrentino/java-async-generator/commit/bace2d332d2ea34857cd18a6b4d886de7e95f52a))
409 |
410 | - add module-info.java ([0bdfa9f7b8ffba6](https://github.com/bsorrentino/java-async-generator/commit/0bdfa9f7b8ffba6854c6de8acdb2b0c05d7f2f60))
411 |
412 | - break jdk8 compatibility move to Jdk17 ([6fa124b34410624](https://github.com/bsorrentino/java-async-generator/commit/6fa124b34410624f17a8061eaa857d441e761345))
413 |
414 |
415 | ### Test
416 |
417 | - add test for reactive stream integration ([09adc3cd805d03d](https://github.com/bsorrentino/java-async-generator/commit/09adc3cd805d03d2923eb54f2bf0cf83a0b6faca))
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 | ## [v2.3.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v2.3.0) (2024-12-02)
427 |
428 | ### Features
429 |
430 | * add map and filter operators ([39ebb0396323aaa](https://github.com/bsorrentino/java-async-generator/commit/39ebb0396323aaa89018e84d0b8a3695c2add7e3))
431 | > Add new AsyncGeneratorOperators interface
432 | > Add async(executor) method to easier manage a provided custom executor
433 |
434 |
435 | ### Bug Fixes
436 |
437 | - **pom-jdk8.xml** update version to SNAPSHOT for async-generator-jdk8 ([1c27cf97df06971](https://github.com/bsorrentino/java-async-generator/commit/1c27cf97df069715fdfab277405800843bfb1b54))
438 |
439 |
440 | ### Documentation
441 |
442 | - update comment ([99681d9380035b5](https://github.com/bsorrentino/java-async-generator/commit/99681d9380035b57e38ed109189dad56f3cea8ef))
443 |
444 | - update changeme ([5ffc6b7bb18bf4d](https://github.com/bsorrentino/java-async-generator/commit/5ffc6b7bb18bf4d0c33d7639897b934e191007d8))
445 |
446 |
447 | ### Refactor
448 |
449 | - update async-generator version to SNAPSHOT ([5ffcf2b873f2963](https://github.com/bsorrentino/java-async-generator/commit/5ffcf2b873f2963d50aab7beb9e33ebc09a9fb01))
450 |
451 |
452 | ### ALM
453 |
454 | - move to next version ([478001f5e078377](https://github.com/bsorrentino/java-async-generator/commit/478001f5e07837754c6d498e91bbf1da7aaa1ede))
455 |
456 | - update actions ([f3d30d3112e0f1a](https://github.com/bsorrentino/java-async-generator/commit/f3d30d3112e0f1a395df85b2bf92f08da3068040))
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 | ## [v2.2.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v2.2.0) (2024-11-10)
467 |
468 | ### Features
469 |
470 | * add support for embed generator ([6efebe3b49b1bbd](https://github.com/bsorrentino/java-async-generator/commit/6efebe3b49b1bbd4a68870cd0b2a705e24084b2e))
471 | > Currently not supported the recursive embed generators (ie embed of embed)
472 |
473 | * add support for nested generator ([c924c8e70268f9d](https://github.com/bsorrentino/java-async-generator/commit/c924c8e70268f9d67fd9a8fd1348b0744f2bcdc1))
474 | > - provide a 'resultValue' on done.
475 |
476 |
477 |
478 | ### Documentation
479 |
480 | - update changeme ([7df0bc31be1eb25](https://github.com/bsorrentino/java-async-generator/commit/7df0bc31be1eb250dc188149501c277c5b60b22a))
481 |
482 |
483 | ### Refactor
484 |
485 | - replace data.done with data.isDone() ([5dffc8ef83e286f](https://github.com/bsorrentino/java-async-generator/commit/5dffc8ef83e286fd46cff2d352ca36b8d7601d8a))
486 |
487 |
488 | ### ALM
489 |
490 | - bump to new version ([323a1944f53d3be](https://github.com/bsorrentino/java-async-generator/commit/323a1944f53d3becb56b6cd6cd0d0575742ad686))
491 |
492 | - bump to next SNAPSHOT ([c080a578df9e515](https://github.com/bsorrentino/java-async-generator/commit/c080a578df9e515eed5e27e010cc772c09f41361))
493 |
494 |
495 |
496 | ### Continuous Integration
497 |
498 | - add script for set project version ([654e3b5f8998ba8](https://github.com/bsorrentino/java-async-generator/commit/654e3b5f8998ba81446818017c8310917098531b))
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 | ## [v2.1.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v2.1.0) (2024-11-05)
507 |
508 | ### Features
509 |
510 | * change visibility ([5d7b5b5a4b096e5](https://github.com/bsorrentino/java-async-generator/commit/5d7b5b5a4b096e56c60b138cb10a39e3f0489a9d))
511 | > publish AsyncGenerator.Data.isDone()
512 | > publish AsyncGeneratorQueue.Generator
513 |
514 |
515 |
516 | ### Documentation
517 |
518 | - update readme ([980d2305fbe011e](https://github.com/bsorrentino/java-async-generator/commit/980d2305fbe011ed1c8c6de48244c06389bb464f))
519 |
520 | - update changelog ([3c0a8344c30b476](https://github.com/bsorrentino/java-async-generator/commit/3c0a8344c30b47655eb39b2fa68d6b6ce4d8c905))
521 |
522 |
523 |
524 | ### ALM
525 |
526 | - bump to next version ([c91e5c5d656c693](https://github.com/bsorrentino/java-async-generator/commit/c91e5c5d656c693fac41349ae01daaccdcad983d))
527 |
528 | - move to next development version ([452b2439f7c5a3f](https://github.com/bsorrentino/java-async-generator/commit/452b2439f7c5a3f40304a276ceb5442f31a59699))
529 |
530 |
531 |
532 | ### Continuous Integration
533 |
534 | - add script for generating changelog ([ed6859b21d2cb2d](https://github.com/bsorrentino/java-async-generator/commit/ed6859b21d2cb2d886184f49b1fc1bd3a82a1760))
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 | ## [v2.0.1](https://github.com/bsorrentino/java-async-generator/releases/tag/v2.0.1) (2024-07-21)
543 |
544 | ### Features
545 |
546 | * **AsyncGenerator** add new Data.of() static method ([09de8d4ff324a8e](https://github.com/bsorrentino/java-async-generator/commit/09de8d4ff324a8eda81f712eb4cdbc11b009fc33))
547 | > make easier to create Data instance don't providing CompletableFuture
548 |
549 |
550 | ### Bug Fixes
551 |
552 | - deployment signing ([80ae69ced67514c](https://github.com/bsorrentino/java-async-generator/commit/80ae69ced67514c104742c4430067ed2939238fb))
553 |
554 |
555 | ### Documentation
556 |
557 | - update javadoc ([dcbd81f1fffae8e](https://github.com/bsorrentino/java-async-generator/commit/dcbd81f1fffae8e27ee6185232511401755dea05))
558 |
559 | - update changelog ([05cfc8f2b448cd9](https://github.com/bsorrentino/java-async-generator/commit/05cfc8f2b448cd9a01537e867b74e9657eb8af01))
560 |
561 |
562 | ### Refactor
563 |
564 | - **AsyncGeneratorQuest** deprecate method of( queue, executor, consumer) ([73d3ed4b65b79f6](https://github.com/bsorrentino/java-async-generator/commit/73d3ed4b65b79f668c239b31632537270843c86c))
565 | > now the standard is of( queue, consumer, executor )
566 |
567 |
568 | ### ALM
569 |
570 | - move to the next release ([61fc7accf77b410](https://github.com/bsorrentino/java-async-generator/commit/61fc7accf77b4102dabc13854a87f762760b2c70))
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 | ## [v2.0.0](https://github.com/bsorrentino/java-async-generator/releases/tag/v2.0.0) (2024-07-15)
581 |
582 | ### Features
583 |
584 | * allow using executor on async implementation. by default ForkJoinPool.commonPool() ([ec470928c243ae9](https://github.com/bsorrentino/java-async-generator/commit/ec470928c243ae969a592689a17ddabeb0445e59))
585 | > - forEachAsync
586 | > - collectAsync
587 |
588 |
589 |
590 | ### Documentation
591 |
592 | - update javadocs ([07798a4bd1d4692](https://github.com/bsorrentino/java-async-generator/commit/07798a4bd1d469278df70ce5ca38a71a14374685))
593 |
594 | - update javadocs ([ec893b8bb4d702b](https://github.com/bsorrentino/java-async-generator/commit/ec893b8bb4d702b60cd5ff8700818719ffc50cc5))
595 |
596 | - add javadoc ([ae7b39f259ce716](https://github.com/bsorrentino/java-async-generator/commit/ae7b39f259ce716967ab0f6b5d947f0893b6da95))
597 |
598 | - add site support ([0801ee206b2031f](https://github.com/bsorrentino/java-async-generator/commit/0801ee206b2031f35e28fe75e3866010485038bb))
599 |
600 | - update readme ([ae2972adf37f99a](https://github.com/bsorrentino/java-async-generator/commit/ae2972adf37f99a22f2f38b578419d05edb1fa55))
601 |
602 | - add changelog ([683b8b74be21c86](https://github.com/bsorrentino/java-async-generator/commit/683b8b74be21c865483a408111a0b6969fe8bc7d))
603 |
604 |
605 |
606 | ### ALM
607 |
608 | - refine jdk8 support ([f8d6870c18ba8d2](https://github.com/bsorrentino/java-async-generator/commit/f8d6870c18ba8d205e58b30414d6286d40b21f51))
609 |
610 | - add jdk8 classifier ([1a5e66caf4877c3](https://github.com/bsorrentino/java-async-generator/commit/1a5e66caf4877c347232b007e563d776b6625ad7))
611 |
612 | - fix deployment process to maven central ([4ec64ec51d1d7f0](https://github.com/bsorrentino/java-async-generator/commit/4ec64ec51d1d7f02f1b638b5ca011d450bf533ce))
613 |
614 | - add description in pom ([2d1c0af0dce602a](https://github.com/bsorrentino/java-async-generator/commit/2d1c0af0dce602a55dd638364867c2c647ca8838))
615 |
616 |
617 |
618 | ### Continuous Integration
619 |
620 | - update deploy-github-pages.yml ([540441b71c9c8ce](https://github.com/bsorrentino/java-async-generator/commit/540441b71c9c8ce791412960f40bfb10aaf2fa53))
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 | ## [v1.0.0-jdk11](https://github.com/bsorrentino/java-async-generator/releases/tag/v1.0.0-jdk11) (2024-04-24)
629 |
630 | ### Features
631 |
632 | * add error propagation ([9468dd1a995d214](https://github.com/bsorrentino/java-async-generator/commit/9468dd1a995d2145eaccc92e12be90a08b131d70))
633 |
634 |
635 |
636 | ### Documentation
637 |
638 | - add javadoc ([554c5f70ee13599](https://github.com/bsorrentino/java-async-generator/commit/554c5f70ee13599db4cdd3902df9a97fec85e115))
639 |
640 | - update readme ([dfdbd70c3da9bc8](https://github.com/bsorrentino/java-async-generator/commit/dfdbd70c3da9bc84d9f861e3668bd85becf59c4e))
641 |
642 | - update README.md ([8824be491f16def](https://github.com/bsorrentino/java-async-generator/commit/8824be491f16def904ace011eabcf29f0c29572f))
643 |
644 |
645 | ### Refactor
646 |
647 | - clean code ([b95ffe647e23b1f](https://github.com/bsorrentino/java-async-generator/commit/b95ffe647e23b1f964d2a05ddc675a5c0d87744e))
648 |
649 | - simplify BlockingQueue ([72aee2f5d9e806f](https://github.com/bsorrentino/java-async-generator/commit/72aee2f5d9e806ffcd3b212836ba51364317ec43))
650 |
651 | - simplify interaction with collections ([bed45e744511e76](https://github.com/bsorrentino/java-async-generator/commit/bed45e744511e76660f1aba163c9b82f77ac9226))
652 | > add static methods empty, map, collect, toCompletableFuture
653 |
654 | - consider completablefuture as argument ([d1310ceafddc653](https://github.com/bsorrentino/java-async-generator/commit/d1310ceafddc65393db037cb4afb63f03157a863))
655 |
656 |
657 | ### ALM
658 |
659 | - add build script using npm ([0e071c0423b2cc5](https://github.com/bsorrentino/java-async-generator/commit/0e071c0423b2cc50f507a581fce27e2b32209474))
660 |
661 | - add changelog management ([6ee16713630faa1](https://github.com/bsorrentino/java-async-generator/commit/6ee16713630faa15c9c21b5205f201ab5edcc865))
662 |
663 | - move to next version ([d3faef43359935c](https://github.com/bsorrentino/java-async-generator/commit/d3faef43359935c0e3b8d4acec9b48a135334f4e))
664 |
665 | - add support for classifier ([44add67e66f96ec](https://github.com/bsorrentino/java-async-generator/commit/44add67e66f96ec636fa7ef4e18631ab1e8e952a))
666 |
667 | - setup deploy asset to maven repo ([0a81096c4d43773](https://github.com/bsorrentino/java-async-generator/commit/0a81096c4d43773368f0305edc9fc8df12500c80))
668 |
669 | - update pom information ([02e8557d7aa17d2](https://github.com/bsorrentino/java-async-generator/commit/02e8557d7aa17d287caf6d654e33d599e76dc560))
670 |
671 |
672 |
673 |
674 |
675 |
--------------------------------------------------------------------------------