├── src ├── site │ ├── markdown │ │ └── .gitkeep │ └── site.xml ├── main │ └── java │ │ ├── module-info.java │ │ └── org │ │ └── bsc │ │ └── async │ │ ├── FlowGenerator.java │ │ ├── internal │ │ ├── reactive │ │ │ ├── GeneratorPublisher.java │ │ │ └── GeneratorSubscriber.java │ │ └── UnmodifiableDeque.java │ │ ├── AsyncGeneratorQueue.java │ │ └── AsyncGenerator.java └── test │ ├── resources │ └── logging.properties │ └── java │ └── org │ └── bsc │ └── async │ ├── Task.java │ ├── FutureCancellationTest.java │ ├── FlowGeneratorTest.java │ ├── AsyncGeneratorQueueTest.java │ └── AsyncGeneratorTest.java ├── gpg-setup.sh ├── hotfix-changelog.sh ├── set-version.sh ├── settings-template.xml ├── package.json ├── changelog.json ├── .gitignore ├── .github └── workflows │ ├── deploy-snapshot.yaml │ ├── deploy-pages.yaml │ ├── deploy.yaml │ └── deploy-github-pages.yml ├── LICENSE ├── GEMINI.md ├── README.md ├── changelog.mustache ├── CANCELLATION.md ├── pom.xml └── CHANGELOG.md /src/site/markdown/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | 2 | module async.generator { 3 | exports org.bsc.async; 4 | } -------------------------------------------------------------------------------- /gpg-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Prepare GPG Key is expected to be in base64 5 | # Exported with gpg -a --export-secret-keys "your@email" | base64 > gpg.base64 6 | # 7 | printf "$GPG_KEY_BASE64" | base64 --decode > gpg.asc 8 | echo ${GPG_PASSPHRASE} | gpg --batch --yes --passphrase-fd 0 --import gpg.asc 9 | gpg -k -------------------------------------------------------------------------------- /hotfix-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # prepare the CHANGELOG.md with the current version 4 | git flow hotfix start changeme 5 | 6 | # generate CHANGELOG.md 7 | git-changelog-command-line -of CHANGELOG.md 8 | 9 | git commit -m'docs: update changeme' -a 10 | 11 | # finish the hotfix without create tag 12 | git flow hotfix finish changeme -n -m"changelog hotfix merge" 13 | -------------------------------------------------------------------------------- /set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if an argument was provided via the command line 4 | if [ $# -gt 0 ]; then 5 | ver="$1" 6 | echo "Argument provided via command line: $ver" 7 | else 8 | # If no argument was provided, prompt the user for input 9 | read -p "Please enter an argument: " user_input 10 | ver="$user_input" 11 | echo "Argument provided by user: $ver" 12 | fi 13 | 14 | mvn versions:set -DnewVersion=$ver 15 | 16 | # after this, you need to commit the changes 17 | # mvn versions:commit -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.apache.maven.skins 4 | maven-fluido-skin 5 | 2.0.0-M8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | .level=FINE 3 | java.util.logging.ConsoleHandler.level=ALL 4 | java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 5 | #java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$s] %5$s %n 6 | java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n 7 | #java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%2$-7s] %3$s %4$s%n 8 | #java.util.logging.SimpleFormatter.format = %1$tc %2$s%n%4$s: %5$s%6$s%n 9 | -------------------------------------------------------------------------------- /settings-template.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | sonatype-central 9 | R4ncrpkA 10 | ${env.OSS_SONATYPE_TOKEN} 11 | 12 | 13 | 14 | github 15 | bsorrentino 16 | ${env.GITHUB_TOKEN} 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-generator", 3 | "version": "2.0.0", 4 | "description": "a Java version of Javascript async generator", 5 | "scripts": { 6 | "gpg:export-key": "gpg -a --export-secret-keys \"bartolomeo.sorrentino@gmail.com\" | base64 > gpg.base64", 7 | "gpg:send-public-key": "gpg --keyserver hkp://keys.openpgp.org --send-keys $GPG_KEY", 8 | "changelog:tag": "mvn git-changelog-maven-plugin:git-changelog -N -DtoRef=refs/tags/v$npm_package_version", 9 | "changelog:release": "mvn git-changelog-maven-plugin:git-changelog -N -DtoRef=refs/heads/release/$npm_package_version" 10 | 11 | }, 12 | "keywords": [], 13 | "author": "bsorrentino (http://soulsoftware-bsc.blogspot.it/)", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/org/bsc/async/Task.java: -------------------------------------------------------------------------------- 1 | package org.bsc.async; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | public class Task { 6 | 7 | static CompletableFuture of(int index, long delayInMills) { 8 | return CompletableFuture.supplyAsync( () -> { 9 | try { 10 | Thread.sleep( delayInMills ); 11 | } catch (InterruptedException e) { 12 | throw new RuntimeException(e); 13 | } 14 | return "e" + index; 15 | } ); 16 | //return completedFuture("e" + index); 17 | } 18 | static CompletableFuture of(int index) { 19 | return CompletableFuture.supplyAsync( () -> { 20 | return "e" + index; 21 | } ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "fromRepo": ".", 3 | "toRef": "refs/heads/main", 4 | //"fromCommit": "0000000000000000000000000000000000000000", 5 | // "ignoreCommitsIfMessageMatches": "^\\[maven-release-plugin\\].*|^\\[Gradle Release Plugin\\].*|^Merge.*", 6 | // "readableTagName": "-([^-]+?)$", 7 | // "dateFormat": "YYYY-MM-dd HH:mm:ss", 8 | "untaggedName": "Next release", 9 | "noIssueName": "Generic changes", 10 | "timeZone": "UTC", 11 | // "removeIssueFromMessage": "true", 12 | 13 | // "jiraServer": "https://jiraserver/jira", 14 | // "jiraIssuePattern": "\\b[a-zA-Z]([a-zA-Z]+)-([0-9]+)\\b", 15 | 16 | // "gitHubApi": "https://api.github.com/repos/bsorrentino/maven-confluence-plugin", 17 | // "gitHubIssuePattern": "#([0-9]+)", 18 | 19 | // "customIssues": [ 20 | // { "name": "Bugs", "pattern": "#bug" }, 21 | // { "name": "Features", "pattern": "#feature" } 22 | // ], 23 | "void":"" 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/ 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store 36 | # Compiled class file 37 | *.class 38 | 39 | # Log file 40 | *.log 41 | 42 | # BlueJ files 43 | *.ctxt 44 | 45 | # Mobile Tools for Java (J2ME) 46 | .mtj.tmp/ 47 | 48 | # Package Files # 49 | *.jar 50 | *.war 51 | *.nar 52 | *.ear 53 | *.zip 54 | *.tar.gz 55 | *.rar 56 | 57 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 58 | hs_err_pid* 59 | replay_pid* 60 | /.java-version 61 | -------------------------------------------------------------------------------- /.github/workflows/deploy-snapshot.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java#apache-maven-with-a-settings-path 3 | 4 | name: Deploy New Snapshot 5 | 6 | on: 7 | workflow_dispatch: 8 | branches: 9 | - develop 10 | 11 | # on: 12 | # push: 13 | # branches: 14 | # - develop 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | ref: develop 25 | - name: Set up JDK 17 26 | uses: actions/setup-java@v1 27 | with: 28 | java-version: 17 29 | 30 | - name: Build with Maven 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | OSS_SONATYPE_TOKEN: ${{ secrets.OSS_SONATYPE_TOKEN }} 34 | run: mvn -B clean source:jar javadoc:jar install central-publishing:publish --file pom.xml -s settings-template.xml 35 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java#apache-maven-with-a-settings-path 3 | 4 | name: Deploy Documentation 5 | 6 | on: workflow_dispatch 7 | # on: 8 | # push: 9 | # tags: 10 | # - 'v*' 11 | 12 | jobs: 13 | docs: 14 | # disable for now 15 | # if: ${{ false }} 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | ref: main 22 | - name: Set up JDK 11 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 11 26 | - name: Deploy site to Github pages 27 | run: | 28 | mvn -B javadoc:javadoc --file pom.xml -s settings-template.xml 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./target/site/apidocs 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy New Version 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: deploy 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | ref: main 16 | - name: GPG Setup 17 | env: 18 | GPG_KEY_BASE64: ${{ secrets.GPG_KEY_BASE64 }} 19 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 20 | run: ./gpg-setup.sh 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v4 23 | with: 24 | distribution: 'liberica' 25 | java-version: '17' 26 | java-package: jdk 27 | - name: Deploy to OSS Sonatype 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | OSS_SONATYPE_TOKEN: ${{ secrets.OSS_SONATYPE_TOKEN }} 31 | run: mvn -B -Prelease source:jar javadoc:jar install central-publishing:publish --file pom.xml -s settings-template.xml -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 bsorrentino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /GEMINI.md: -------------------------------------------------------------------------------- 1 | as my java architect, concerning this project named java-async-generator i need that you create documentation about the new feature allowing to cancel an iteration during its execution. the result must be put in `CANCELLATION.md` 2 | 3 | The implementation is in the source file @src/main/java/org/bsc/async/AsyncGenerator.java. 4 | 5 | To understand how to use use the Unit Test in the file @src/test/java/org/bsc/async/AsyncGeneratorTest.java and take a look to method `asyncGeneratorForEachCancelTest()` and `asyncEmbedGeneratorWithResultCancelTest()` 6 | 7 | Explain in the clear technical way that: 8 | 9 | * Cacellation is achieved invoking `cancel(boolean mayInterruptIfRunning)` method on an `AsyncGenerator` that is compliant with `IsCancellable` interface 10 | * Invoking `AsyncGenerator.forEachAsync()` will be create a new single thread where will be executed entire iteration over generator values 11 | * Invoking `AsyncGenerator.iterator()` will not create any thread and iteration will be executed in the current thread 12 | * Invoking `IsCancellable.cancel(false)` set an internal state that will stop iteration when a next value is required 13 | * Invoking `IsCancellable.cancel(true)` set an internal state and try to interrupt the thread on which the iteration is executing 14 | * how to check if iteration has been interrupted 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy-github-pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: generate site 35 | run: | 36 | mvn -B site:site --file pom.xml -s settings-template.xml 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v5 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | # Upload entire repository 43 | path: 'target/site' 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # java-async-generator 5 | 6 | A Java version of Javascript async generator. 7 | Idea is to create an iterator-like interface that emit elements as [CompletableFuture] ( the Java counterpart of Javascript Promise ) enabling asynchronous iteration over data 8 | 9 | 10 | ## Releases 11 | 12 | **Note: ‼️** 13 | > From release 3.0.0 the miminum supported Java version is the `Java 17` and 14 | > will not be longer available the artifact `async-generator-jdk8` 15 | 16 | 17 | ## Installation 18 | 19 | **Maven** 20 | ```xml 21 | 22 | org.bsc.async 23 | async-generator 24 | 4.0.0-beta2 25 | 26 | ``` 27 | 28 | ## Samples 29 | 30 | ### Create an Async Generator to make multiple API calls 31 | 32 | ```java 33 | 34 | AsyncGenerator makeMultipleApiCalls(List requestsData) { 35 | return AsyncGenerator.map(requestsData, requestData -> { 36 | 37 | CompletableFuture res = asyncApiCall( requestData ); 38 | 39 | return res; 40 | }); 41 | 42 | } 43 | 44 | List resquestsData = .... 45 | 46 | // can iterate using lambda function (Consumer) 47 | makeMultipleApiCalls( resquestsData ) 48 | .forEachAsync( response -> logger.info( "Api response: " + response ) ) 49 | .join(); 50 | 51 | // can iterate using classic for( : ) 52 | AsyncGenerator generator = makeMultipleApiCalls( resquestsData ); 53 | 54 | for( Response response : generator ) { 55 | logger.info( "Api response: " + response ) 56 | } 57 | 58 | ``` 59 | ## Cancellation 60 | 61 | Take a look the new [Cancellation](CANCELLATION.md) feature 62 | 63 | ## Articles: 64 | 65 | * [How to stream data over HTTP using Java Servlet and Fetch API](https://bsorrentino.github.io/bsorrentino/web/2024/07/21/how-to-stream-data-in-java.html) 66 | 67 | [CompletableFuture]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html 68 | -------------------------------------------------------------------------------- /src/main/java/org/bsc/async/FlowGenerator.java: -------------------------------------------------------------------------------- 1 | package org.bsc.async; 2 | 3 | import org.bsc.async.internal.reactive.GeneratorPublisher; 4 | import org.bsc.async.internal.reactive.GeneratorSubscriber; 5 | 6 | import java.util.concurrent.Flow; 7 | import java.util.concurrent.LinkedBlockingQueue; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * Provides methods for converting between {@link FlowGenerator} and various {@link java.util.concurrent.Flow.Publisher} types. 12 | * 13 | * @since 3.0.0 14 | */ 15 | public interface FlowGenerator { 16 | 17 | /** 18 | * Creates an {@code AsyncGenerator} from a {@code Flow.Publisher}. 19 | * 20 | * @param 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 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 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 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 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 | --------------------------------------------------------------------------------