Package Exports
- @shoru/listrx
- @shoru/listrx/src/index.mjs
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@shoru/listrx) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
๐ ListrX
Beautiful CLI task management with runtime task injection
A powerful Node.js utility that combines listr2's beautiful CLI output with RxJS's reactive streams to create an easy to use dynamic task queue system. Add tasks from anywhere in your code (e.g. event handlers, API callbacks, or async workflows) and watch them execute with elegant terminal feedback.
Features โข Installation โข Quick Start โข API โข Examples
โจ Features
๐ฏ Core Capabilities
|
๐ ๏ธ Developer Experience
|
๐ฆ Installation
# Using npm
npm install @shoru/listrx
# Using yarn
yarn add @shoru/listrx
# Using pnpm
pnpm add @shoru/listrxPrerequisites
- Node.js 18.0.0 or higher
- ES Modules support (the package uses
.mjsextensions)
Peer Dependencies
The library requires these packages (installed automatically):
{
"listr2": "^9.0.5",
"rxjs": "^7.8.2"
}๐ Quick Start
The Simplest Example
import { createQueue } from '@shoru/listrx';
// Create a queue
const queue = createQueue();
// Add tasks from anywhere!
queue.add('Say Hello', async () => {
console.log('Hello, World!');
});
queue.add('Do Something Async', async () => {
await fetch('https://api.example.com/data');
});
// Signal completion when done adding tasks
await queue.complete();What You'll See
โ Say Hello
โ Do Something Async๐ API Reference
Factory Functions
| Function | Description | Returns |
|---|---|---|
createQueue(options?) |
Create a new independent queue instance | DynamicTaskQueue |
getQueue(options?) |
Get/create singleton instance | DynamicTaskQueue |
resetQueue() |
Reset the singleton instance | Promise<void> |
addTask(title, fn) |
Shorthand for getQueue().add() |
Promise<any> |
createQueue(options?)
Creates a new task queue instance with the specified configuration.
import { createQueue } from '@shoru/listrx';
const queue = createQueue({
concurrent: true,
exitOnError: false,
batchDebounceMs: 50,
rendererOptions: {
showTimer: true
}
});Options
| Option | Type | Default | Description |
|---|---|---|---|
concurrent |
boolean |
false |
Run tasks in parallel instead of sequentially |
exitOnError |
boolean |
false |
Stop the entire queue if a task fails |
batchDebounceMs |
number |
50 |
Milliseconds to wait before processing a batch |
rendererOptions |
object |
{} |
Listr2 renderer configuration |
Renderer Options
{
rendererOptions: {
showTimer: true, // Show duration for each task
collapseSubtasks: false, // Keep subtasks expanded
showSubtasks: true, // Display subtask details
removeEmptyLines: false, // Preserve spacing
formatOutput: 'wrap' // Output text wrapping
}
}Instance Methods
queue.add(title, taskFn)
Add a task to the queue. Returns a promise that resolves with the task's return value.
const result = await queue.add('Process Data', async (ctx, task) => {
task.output = 'Working...';
const data = await processData();
return data;
});
console.log(result); // Whatever processData() returnedParameters:
| Parameter | Type | Description |
|---|---|---|
title |
string |
Display title shown in the terminal |
taskFn |
(ctx, task) => Promise<any> |
Async function to execute |
Task Function Arguments:
| Argument | Type | Description |
|---|---|---|
ctx |
object |
Shared context object across all tasks in a batch |
task |
ListrTaskWrapper |
Listr task object for output/title updates |
Task Object Properties:
queue.add('Example', async (ctx, task) => {
task.output = 'Status message'; // Update the task's output line
task.title = 'New Title'; // Change the task's title
// Skip the task
task.skip('Reason for skipping');
// Access Listr's full API
task.stdout(); // Get stdout stream
});queue.addMany(tasks)
Add multiple tasks at once. Returns a promise that resolves with all results.
const results = await queue.addMany([
{ title: 'Task 1', task: async () => 'result1' },
{ title: 'Task 2', task: async () => 'result2' },
{ title: 'Task 3', task: async () => 'result3' }
]);
console.log(results); // ['result1', 'result2', 'result3']queue.complete()
Signal that no more tasks will be added and wait for all pending tasks to finish.
// Add your tasks...
queue.add('Task 1', async () => { /* ... */ });
queue.add('Task 2', async () => { /* ... */ });
// Wait for everything to complete
await queue.complete();
console.log('All done!');โ ๏ธ Important: After calling
complete(), no new tasks can be added to the queue.
queue.forceShutdown(reason?)
Immediately stop the queue and reject all pending tasks.
// Gracefully handle SIGINT
process.on('SIGINT', () => {
queue.forceShutdown('User interrupted');
process.exit(1);
});Instance Properties
| Property | Type | Description |
|---|---|---|
state |
string |
Current state: 'idle' | 'processing' | 'completing' | 'completed' |
state$ |
Observable<string> |
RxJS Observable of state changes |
isIdle |
boolean |
true if queue is idle |
isProcessing |
boolean |
true if queue is processing tasks |
isCompleted |
boolean |
true if queue has completed |
pendingCount |
number |
Number of tasks waiting to be processed |
stats |
object |
Statistics: { processed, failed, pending } |
Reactive State Monitoring
import { filter } from 'rxjs/operators';
// Subscribe to all state changes
queue.state$.subscribe(state => {
console.log(`Queue is now: ${state}`);
});
// React to specific states
queue.state$.pipe(
filter(state => state === 'completed')
).subscribe(() => {
console.log('Queue finished!');
});๐ Examples
๐น Basic Sequential Tasks
import { createQueue } from 'listx';
async function deployApplication() {
const queue = createQueue();
queue.add('๐ฆ Installing dependencies', async (ctx, task) => {
await runCommand('npm install');
task.output = 'Installed 847 packages';
});
queue.add('๐จ Building application', async (ctx, task) => {
task.output = 'Compiling TypeScript...';
await runCommand('npm run build');
task.output = 'Build complete!';
});
queue.add('๐งช Running tests', async (ctx, task) => {
const results = await runCommand('npm test');
task.output = `${results.passed} passed, ${results.failed} failed`;
ctx.testResults = results;
});
queue.add('๐ Deploying', async (ctx, task) => {
if (ctx.testResults.failed > 0) {
task.skip('Skipping deploy due to test failures');
return;
}
await deploy();
});
await queue.complete();
}๐น Concurrent Task Processing
import { createQueue } from 'listx';
async function processImages(images) {
const queue = createQueue({
concurrent: true // Process all images in parallel!
});
for (const image of images) {
queue.add(`๐ผ๏ธ Process ${image.name}`, async (ctx, task) => {
task.output = 'Resizing...';
await resize(image);
task.output = 'Optimizing...';
await optimize(image);
task.output = 'Uploading...';
await upload(image);
return { name: image.name, status: 'complete' };
});
}
await queue.complete();
}๐น Event-Driven Task Injection
Perfect for file watchers, webhooks, or any event-based workflow:
import { createQueue } from 'listx';
import { watch } from 'chokidar';
// Create a long-running queue
const queue = createQueue({ concurrent: true });
// Watch for file changes
const watcher = watch('./src/**/*.ts');
watcher.on('change', (path) => {
// Dynamically add a task when a file changes!
queue.add(`๐ Rebuild ${path}`, async (ctx, task) => {
task.output = 'Compiling...';
await compile(path);
task.output = 'Running tests...';
await runTests(path);
});
});
// Handle graceful shutdown
process.on('SIGINT', async () => {
watcher.close();
await queue.complete();
process.exit(0);
});๐น With RxJS Streams
Leverage the full power of RxJS:
import { createQueue } from 'listx';
import { interval, fromEvent } from 'rxjs';
import { take, map, mergeMap } from 'rxjs/operators';
const queue = createQueue();
// Create a stream of tasks from an interval
interval(1000).pipe(
take(5),
map(i => ({
title: `โฐ Scheduled task #${i + 1}`,
work: async () => {
await performScheduledWork(i);
}
}))
).subscribe(({ title, work }) => {
queue.add(title, work);
});
// Also handle DOM events (in Electron, for example)
fromEvent(button, 'click').pipe(
mergeMap(() => queue.add('๐ฑ๏ธ Handle click', async () => {
await processClick();
}))
).subscribe();๐น Error Handling
import { createQueue } from 'listx';
const queue = createQueue({ exitOnError: false });
// Method 1: Handle errors per-task
queue.add('Risky Operation', async () => {
throw new Error('Something went wrong!');
}).catch(error => {
console.error('Task failed:', error.message);
});
// Method 2: Try-catch inside the task
queue.add('Safe Operation', async (ctx, task) => {
try {
await riskyOperation();
} catch (error) {
task.output = `Failed: ${error.message}`;
// Optionally skip or handle gracefully
}
});
// Method 3: Track all results
const results = await Promise.allSettled([
queue.add('Task 1', async () => 'success'),
queue.add('Task 2', async () => { throw new Error('fail'); }),
queue.add('Task 3', async () => 'success')
]);
console.log(results);
// [
// { status: 'fulfilled', value: 'success' },
// { status: 'rejected', reason: Error('fail') },
// { status: 'fulfilled', value: 'success' }
// ]๐น Using the Singleton Pattern
For application-wide task management:
// queue.mjs โ Setup file
import { getQueue } from 'listx';
export const queue = getQueue({
concurrent: true,
rendererOptions: { showTimer: true }
});// anywhere-in-your-app.mjs
import { addTask } from 'listx';
// Uses the same singleton instance!
addTask('Background Job', async () => {
await doWork();
});// main.mjs โ Entry point
import { getQueue } from 'listx';
import './setup-event-handlers.mjs';
// When your app is shutting down
process.on('beforeExit', async () => {
await getQueue().complete();
});๐น Progress Updates and Subtasks
queue.add('๐ Processing large dataset', async (ctx, task) => {
const items = await fetchItems();
const total = items.length;
for (let i = 0; i < items.length; i++) {
// Update progress in the output
task.output = `Processing item ${i + 1}/${total} (${Math.round((i/total) * 100)}%)`;
await processItem(items[i]);
}
task.output = `Completed ${total} items`;
});๐ง Advanced Configuration
Custom Renderer
import { createQueue } from 'listx';
// Use the "simple" renderer for CI environments
const queue = createQueue({
renderer: process.env.CI ? 'simple' : 'default',
rendererOptions: {
showTimer: true,
formatOutput: 'wrap'
}
});Conditional Renderer Selection
import { createQueue } from 'listx';
const queue = createQueue({
renderer: process.env.CI ? 'simple' : 'default',
rendererFallback: 'simple', // Fallback for non-TTY
rendererSilent: process.env.SILENT === 'true'
});๐ State Machine
The queue follows a predictable state lifecycle:
โโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โผ โ
โโโโโโโโโ add() โโโโโโโโโโโโโโ (batch complete, more pending)
โ IDLE โ โโโโโโโโบ โ PROCESSING โ โโโโ
โโโโโโโโโ โโโโโโโโโโโโโโ
โฒ โ
โ โ complete()
โ โผ
โ โโโโโโโโโโโโโโ
โโโโโโโโโโโโโ โ COMPLETING โ
(new tasks) โโโโโโโโโโโโโโ
โ
โ (all tasks done)
โผ
โโโโโโโโโโโโโโ
โ COMPLETED โ
โโโโโโโโโโโโโโ| State | Description |
|---|---|
idle |
Queue is empty and waiting for tasks |
processing |
Actively running tasks |
completing |
complete() called, finishing remaining tasks |
completed |
All tasks finished, queue is closed |
๐งช Testing
When testing code that uses the task queue:
import { createQueue } from 'listx';
describe('MyFeature', () => {
let queue;
beforeEach(() => {
// Create a fresh queue for each test
queue = createQueue({
renderer: 'silent' // Disable output during tests
});
});
afterEach(async () => {
// Always clean up
if (!queue.isCompleted) {
await queue.complete();
}
});
it('should process tasks', async () => {
const result = await queue.add('Test Task', async () => {
return 42;
});
expect(result).toBe(42);
});
});๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/Mangaka-bot/ListrX.git
cd listx
npm install
npm run example:basic๐ License
This project is licensed under the MIT License โ see the LICENSE file for details.
๐ Acknowledgments
- listr2 โ For the beautiful terminal task interface
- RxJS โ For reactive streams that power the queue
Made with โค๏ธ for the Node.js CLI community