Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

71 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jaewa Command Chain

A lightweight Java 11+ library designed to orchestrate complex algorithms as a sequence of asynchronous steps. The core philosophy is that each step (Command) is responsible for its own completion, signaling the progression to the next phase via manual flow control.

Key Concept: Asynchronous Step Orchestration

Unlike traditional linear execution, command-chain allows you to model algorithms where each step might involve asynchronous operations (I/O, timers, external events). A step is considered "finished" only when it explicitly calls next() on the chain controller.

Features

  • Manual Flow Control: Total control over algorithm progression using chain.next() and chain.fail().
  • Active Command Protection: Commands can only affect the chain (next(), fail(), add()) while they are the currently active command; late or duplicate calls are safely ignored.
  • Asynchronous by Design: Ideal for simulating complex state machines or multi-step processes where steps finish at different times.
  • Fluent Algorithm Builder: Compose your logic using a clean, readable builder API.
  • Dynamic Command Addition: Add new commands to the chain even during execution, allowing for adaptive workflows.
  • Native Loop Support: Built-in support for repetitive tasks (ForLoop, TimedLoop) that integrate seamlessly with the asynchronous flow.
  • Robust Error Propagation: Centralized failure handling that catches both synchronous exceptions and manual failure signals.
  • Thread Efficiency: Optimized for non-blocking execution, utilizing threads only when necessary.

Installation

Add the following dependency to your pom.xml:

<dependency>
<groupId>com.jaewa</groupId>
<artifactId>command-chain</artifactId>
<version>1.1.0</version>
</dependency>

General Description

CommandExecutor and Command Sources

The CommandExecutor is the heart of the system. It manages a collection of commands and coordinates their execution. There are two primary modes of operation based on the CommandSource used:

  1. CommandPipeline (Default): Commands remain in the collection after being executed. This is ideal for re-running the same sequence of steps multiple times.
  2. CommandQueue: Commands are removed from the collection once they are executed. This is perfect for producer-consumer scenarios or background workers where tasks are processed and then discarded.

Commands: Async vs Sync

The library provides two fundamental ways to define steps in your workflow: AsyncCommand and Command.

1. AsyncCommand (Explicit Flow Control)

AsyncCommand is the foundational building block of the library:

@FunctionalInterfacepublicinterfaceAsyncCommand {
voidexecute(Contextctx, CommandChainchain) throwsException;
}

An AsyncCommand is not asynchronous by itself—it simply receives the execution context (Context) and the flow controller (CommandChain). However, it is what enables the algorithm execution to become asynchronous. The chain execution stops at this step until the command explicitly signals completion by calling chain.next() or reports an error via chain.fail(throwable).

Simple AsyncCommand without Asynchrony

An AsyncCommand does not require background threads or CompletableFuture. It can simply perform a direct action and then manually advance the chain:

// Simple AsyncCommand: performs an action and explicitly calls next()AsyncCommandsimpleStep = (ctx, chain) -> {
System.out.println("Executing simple step...");
ctx.set("status", "in_progress");
chain.next(); // Explicitly advance to the next command
};

2. Command (Synchronous & Automatic Flow Control)

For standard, synchronous operations where you don't need manual flow control, you can use Command:

@FunctionalInterfacepublicinterfaceCommand {
voidexecute(Contextctx) throwsException;
}

Command can be used directly without dealing with the CommandChain:

// Synchronous Command: no need to call chain.next()CommandsyncStep = ctx -> {
System.out.println("Executing synchronous step...");
ctx.set("key", "value");
};
How Command works under the hood

When you pass a Command to exec() (or use Commands.async(cmd)), it is automatically converted into an AsyncCommand under the hood:

  • When the execute(ctx) method returns normally, chain.next() is called automatically.
  • If the method throws an exception, it is caught and chain.fail(exception) is called automatically.

Conceptually, the adaptation works as follows:

// Behind the scenes conversion (Commands.async(cmd))
(ctx, chain) -> {
try {
cmd.execute(ctx);
chain.next(); // Automatically advances on success
} catch (Exceptione) {
chain.fail(e); // Automatically fails on exception
}
}

3. Asynchronous Operations with CompletableFuture

The true power of AsyncCommand becomes evident when performing asynchronous or non-blocking tasks (such as HTTP calls, database queries, timers, or background processing). Because the chain only progresses when chain.next() is invoked, you can easily delegate chain.next() or chain.fail() to asynchronous callbacks:

// Using an AsyncCommand with an asynchronous service
(ctx, chain) -> {
externalService.callAsync(data)
.thenAccept(result -> {
ctx.set("result", result);
chain.next(); // Continue to next step ONLY when API returns
})
.exceptionally(ex -> {
chain.fail(ex); // Signal failure if API failsreturnnull;
});
}

Or using handle:

(ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex); // Propagate error to the chain
} else {
ctx.set("data", res);
chain.next(); // Proceed with the result
}
returnnull;
});
}

Advantages:

  • No Blocked Threads: The system does not use any thread while waiting for the external API to complete. No thread is put in wait() state.
  • Resource Efficiency: You can handle thousands of concurrent chains with a very small thread pool.

4. Passing CompletableFuture Directly (Commands.async)

If you already have a CompletableFuture, you don't need to manually write callback boilerplate with handle or thenAccept. You can pass it directly to exec() using Commands.async(future) (or with static import async(future)):

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> future = someService.fetchData();
CommandExecutor.pipelineBuilder()
// Automatically calls chain.next() on completion, or chain.fail(e) on failure
.exec(async(future))
.build();

Under the hood, Commands.async(future) automatically registers completion handlers on the future:

(ctx, chain) -> future.whenComplete((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
chain.next();
}
});

5. Active Command Enforcement & Single-Use Flow Control

To protect against race conditions, duplicate progression, and stray or delayed asynchronous callbacks, the CommandExecutor enforces strict rules on the CommandChain:

  • Active Command Only: A command can only interact with the CommandChain (calling next(), fail(), or add()) while it is the currently executing (active) command. If a command attempts to invoke next(), fail(), or add() when it is no longer the active command (e.g. after the chain has already progressed or completed), the call is safely ignored.
  • Single-Use next() and fail(): Each command execution can invoke chain.next() or chain.fail() at most once. Subsequent or duplicate calls by the same command are ignored.
// Example: Delayed callbacks or duplicate calls are safely ignored
(ctx, chain) -> {
chain.next(); // Advances the chain; this command is no longer active// Any subsequent call or late callback from this command is ignored:chain.next(); // Ignoredchain.fail(newRuntimeException("Late error")); // Ignored
};

Fluent Builder API

The library provides a fluent builder to compose complex algorithm chains.

Simple Chains

You can build chains using synchronous commands, asynchronous commands, or wrapped CompletableFuture instances.

importstaticcom.jaewa.commandchain.Commands.async;
CompletableFuture<String> externalFuture = someService.fetchData();
CommandExecutor.pipelineBuilder()
// 1. Synchronous command (Command - auto-next and auto-fail on exception)
.exec(ctx -> System.out.println("Step 1: Sync"))
// 2. Simple Asynchronous command (AsyncCommand - manual next)
.exec((ctx, chain) -> {
System.out.println("Step 2: Simple Async");
ctx.set("step", 2);
chain.next();
})
// 3. Asynchronous command with background work
.exec((ctx, chain) -> {
System.out.println("Step 3: Async start");
CompletableFuture.runAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedExceptione) {}
System.out.println("Step 3: Async end");
chain.next();
});
})
// 4. Elaborate AsyncCommand with CompletableFuture handling
.exec((ctx, chain) -> {
CompletableFuture<String> future = someService.fetchData();
future.handle((res, ex) -> {
if (ex != null) {
chain.fail(ex);
} else {
ctx.set("data", res);
chain.next();
}
returnnull;
});
})
// 5. Directly passing a CompletableFuture via Commands.async
.exec(async(externalFuture))
.build()
.start(newDefaultContext());

exec() Behavior

The exec() method (available on the builder) can receive:

  • Command (ctx -> ...): Executed synchronously. The builder automatically adapts it into an AsyncCommand (via Commands.async(cmd)) which calls chain.next() upon completion and chain.fail(e) if an exception occurs.
  • AsyncCommand ((ctx, chain) -> ...): Gives explicit flow control. Progression requires calling chain.next(), while errors are reported via chain.fail(e).
  • CompletableFuture<?> (via Commands.async(future) / async(future)): Wraps the future into an AsyncCommand. When the future completes normally, chain.next() is called automatically; when it completes exceptionally, chain.fail(e) is called automatically.
  • Runnable (via Commands.async(runnable) or wiretap(runnable)): Can be adapted into an AsyncCommand or run as an independent side-effect.

Wiretap (Side-effects)

The wiretap() method allows you to inject side-effects into the chain without interfering with the main execution flow. It takes a Runnable that is executed in a parallel thread, while the system immediately moves to the next command without waiting. This is perfect for logging, metrics, or monitoring.

CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("status", "processing"))
.wiretap(() -> logger.info("Status set to processing"))
.exec(someAsyncCommand)
.build();

CommandExecutor as the Engine

The builder creates a CommandExecutor instance. To start the execution, you call the start(Context) method.

CommandExecutorexecutor = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Hello"))
.build();
CompletableFuture<Void> future = executor.start(newDefaultContext());
future.thenRun(() -> System.out.println("Chain finished successfully"));
future.exceptionally(ex -> {
// CompletableFuture wraps the original exception in a CompletionExceptionThrowableoriginalCause = ex.getCause() != null ? ex.getCause() : ex;
System.err.println("Chain failed: " + originalCause.getMessage());
returnnull;
});

The start() method returns a CompletableFuture that:

  • Completes successfully when the entire chain finishes without errors.
  • Completes with an exception if chain.fail(throwable) is called somewhere in the chain and the error is not handled (e.g., via onFailure with chain.next() or a doCatch block).

Note that since CompletableFuture is used, the exception passed to exceptionally or handle is typically a java.util.concurrent.ExecutionException. You can retrieve the original error thrown by your command using ex.getCause().

Nested Executors (Sub-blocks)

A CommandExecutor is itself an AsyncCommand. This means you can pass an executor to the exec() method of another builder. This allows you to create "function calls" or reusable sub-blocks of logic.

CommandExecutorsubBlock = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Inside sub-block"))
.build();
CommandExecutormain = CommandExecutor.pipelineBuilder()
.exec(ctx -> System.out.println("Main start"))
.exec(subBlock) // subBlock runs as a command
.exec(ctx -> System.out.println("Main end"))
.build();
main.start(newDefaultContext());

Context and Scoping

The Context (and its implementation DefaultContext) is a hierarchical space for variables. It acts like a programming language's scope.

Variable Visibility

When a CommandExecutor starts, it creates a new context that encapsulates the context passed by the user or the parent executor.

  • Read Access: A command can read variables from its own context and all parent contexts.
  • Write Isolation: When a command calls ctx.set(), the variable is stored in the current context. Parent contexts are never modified.
  • Shadowing: If you set a variable with the same name as one in the parent, you "shadow" it within the current scope.
CommandExecutor.pipelineBuilder()
.exec(ctx -> ctx.set("var", "parent"))
.exec(CommandExecutor.pipelineBuilder()
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Prints "parent"ctx.set("var", "child"); // Shadows parent varSystem.out.println(ctx.get("var", String.class)); // Prints "child"
})
.build())
.exec(ctx -> {
System.out.println(ctx.get("var", String.class)); // Still prints "parent"!
})
.build()
.start(newDefaultContext());

Error Handling

onFailure Handler

You can define an error handler for the executor using onFailure(). The handler can receive the exception and the CommandChain.

  • chain.next(): Swallows the error and allows execution to continue without errors.
  • chain.fail(ex): Propagates the error or throws a new one.
CommandExecutor.pipelineBuilder()
.exec(ctx -> { thrownewRuntimeException("Oops"); })
.onFailure((ex, chain) -> {
System.out.println("Handling error: " + ex.getMessage());
chain.next(); // Chain finishes cleanly
})
.build();

Advanced Error Handling: doTry, doCatch, doFinally

For complex logic, use the try-catch-finally constructs. These catch errors occurring within their block, including those re-thrown by internal onFailure handlers.

CommandExecutor.pipelineBuilder()
.doTry()
.exec(ctx -> { thrownewIOException("Disk Full"); })
.doCatch(IOException.class)
.exec(ctx -> System.out.println("Recovered from IO error"))
.doFinally()
.exec(ctx -> System.out.println("Cleanup successful"))
.end()
.build();

Building Blocks

Loops

The builder supports loop(AbstractLoop). Native implementations include:

  • ForLoop: Standard iteration (init, condition, update).
  • TimedLoop: Runs for a specific duration (milliseconds).
CommandExecutor.pipelineBuilder()
.loop(newForLoop<>("i", () -> 0, i -> i < 5, i -> i + 1))
.exec(ctx -> {
ForLoop<Integer> loop = ctx.get("i", ForLoop.class);
System.out.println("Iteration: " + loop.getValue());
})
.end()
.build();

Choice (Conditional Branching)

The choice() construct allows for when() and otherwise() branches.

CommandExecutor.pipelineBuilder()
.choice()
.when(ctx -> ctx.get("val", Integer.class) > 10)
.exec(ctx -> System.out.println("Greater than 10"))
.end()
.otherwise()
.exec(ctx -> System.out.println("Smaller or equal to 10"))
.end()
.end()
.build();

Command Decorators (Commands class)

The Commands utility class provides static decorators to wrap logic:

  • async(...): Wraps Runnables, Commands, or CompletableFutures into an AsyncCommand.
  • onEventQueue(...): Forces execution on the AWT Event Dispatch Thread (UI).
  • wireTap(Runnable): Executes a side-effect without blocking the main chain progression.
  • conditional(Predicate, AsyncCommand): Executes the command only if the condition is met.
  • logged(String, AsyncCommand): Assigns a name for logging.
  • withTimeout(long, TimeUnit, ...): Wraps a Command or AsyncCommand with a maximum execution timeout, failing the chain with CommandTimeoutException if it does not complete in time.
  • safe(AsyncCommand): Wraps a command to catch exceptions and signal failure automatically.

Example:

importstaticcom.jaewa.commandchain.Commands.*;
builder.exec(onEventQueue(ctx -> label.setText("Updating UI...")))
.exec(wireTap(() -> logger.info("Step reached")))
.exec(logged("FetchData", async(api::call)))
.exec(withTimeout(5, TimeUnit.SECONDS, (ctx, chain) -> {
// Asynchronous task that must call chain.next() or fail() within 5 secondsapi.fetchDataAsync().thenAccept(result -> {
ctx.set("data", result);
chain.next();
}).exceptionally(ex -> {
chain.fail(ex);
returnnull;
});
}));

Continuous Execution Mode

In continuous mode, the executor stays alive and waits for new commands even after finishing the current ones.

CommandExecutorexecutor = newCommandExecutor();
Future<Void> status = executor.startContinuous(newDefaultContext());
// Add commands at runtimeexecutor.add(ctx -> System.out.println("Dynamic command 1"));
// Check statusif (status.isDone()) {
// This happens if someone calls executor.interrupt()
}

The startContinuous method returns a Future that allows you to monitor the executor's lifecycle and wait for its eventual termination.


Manual CommandExecutor Usage

If you prefer not to use the builder, you can configure the CommandExecutor manually.

Simple Chain

CommandExecutorexecutor = newCommandExecutor(newCommandPipeline());
executor.add(ctx -> System.out.println("Manual Step 1"));
executor.add((ctx, chain) -> {
CompletableFuture.runAsync(() -> {
System.out.println("Manual Step 2");
chain.next();
});
});
executor.start(newDefaultContext());

Manual Loop

CommandExecutorexecutor = newCommandExecutor();
ForLoop<Integer> loop = newForLoop<>("i", () -> 0, i -> i < 3, i -> i + 1);
loop.add(ctx -> System.out.println("Manual Loop Iteration"));
executor.add(loop);
executor.start(newDefaultContext());

Manual Try-Catch

TryCatchCommandtryCatch = newTryCatchCommand();
tryCatch.add(ctx -> { thrownewRuntimeException("Error"); });
tryCatch.doCatch(RuntimeException.class);
tryCatch.add(ctx -> System.out.println("Caught!"));
executor.add(tryCatch);

Thread Management and Execution

The library uses an internal ExecutorService to manage execution. Each command is executed on the first available thread from the underlying thread pool.

Customizing the Executor

By default, the library uses a cached thread pool. You can change the type of Executor used by the system via ExecutorService.setExecutorSupplier():

importcom.jaewa.commandchain.service.ExecutorService;
importjava.util.concurrent.Executors;
// Use a fixed thread poolExecutorService.setExecutorSupplier(() -> Executors.newFixedThreadPool(4));
// Or use virtual threads (Java 21+)ExecutorService.setExecutorSupplier(Executors::newVirtualThreadPerTaskExecutor);

This flexibility allows you to tune the performance based on your environment and the nature of your commands (CPU-bound vs I/O-bound).


Developed with ❤️ by Jaewa.

About

A lightweight library designed to orchestrate complex algorithms as a sequence of asynchronous steps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages