JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 11
  • Score
    100M100P100Q41758F
  • License MIT

Dynamic CLI task queue using listr2 and RxJS

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

Node.js listr2 RxJS License

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


Terminal Demo

โœจ Features

๐ŸŽฏ Core Capabilities

  • Dynamic Task Injection โ€” Add tasks at runtime from anywhere
  • RxJS-Powered โ€” Built on reactive streams for maximum flexibility
  • Beautiful Output โ€” Leverages listr2's polished terminal UI
  • Promise-Based โ€” Every task returns a trackable promise
  • Automatic Batching โ€” Groups rapidly-added tasks efficiently

๐Ÿ› ๏ธ Developer Experience

  • Minimal API โ€” Just queue.add(title, fn) to get started
  • TypeScript Ready โ€” Full JSDoc annotations included
  • State Observable โ€” React to queue state changes
  • Graceful Shutdown โ€” Clean completion handling
  • Singleton Support โ€” Optional global instance pattern

๐Ÿ“ฆ Installation

# Using npm
npm install @shoru/listrx

# Using yarn
yarn add @shoru/listrx

# Using pnpm
pnpm add @shoru/listrx

Prerequisites

  • Node.js 18.0.0 or higher
  • ES Modules support (the package uses .mjs extensions)

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() returned

Parameters:

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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

โฌ† Back to Top