Skip to content

Repository files navigation

@cldmv/holdmytask

npm versionnpm downloadsGitHub downloadsLast commitnpm last update

ContributorsSponsor shinrai

A tiny, dependency-free task queue for Node.js that executes tasks with priority ordering, concurrency control, and completion delays. Perfect for managing asynchronous workflows with sophisticated timing requirements.

✨ Features

  • Smart scheduling - Dynamic timeout-based scheduling for optimal performance
  • Task coalescing - Intelligent merging of similar tasks for efficiency
  • Priority-based execution - Higher priority tasks run first
  • Concurrency control - Limit simultaneous task execution
  • Completion delays - Configurable delays between task completions
  • Delay bypass - Urgent tasks can skip active delay periods
  • Timeout support - Automatic task cancellation on timeout
  • Dual API support - Both callback and Promise-based APIs
  • AbortController integration - Cooperative cancellation support
  • Dual ESM/CJS exports - Works with both module systems
  • TypeScript ready - Full type definitions included
  • Zero dependencies - Lightweight and fast

📦 Installation

npm install @cldmv/holdmytask

� Import Options

The library provides flexible import options for different use cases:

Production (Default)

// Named importimport{HoldMyTask}from"@cldmv/holdmytask";// Default importimportHoldMyTaskfrom"@cldmv/holdmytask";// Import everythingimport*asHoldMyTaskLibfrom"@cldmv/holdmytask";// Uses optimized distribution files

Development Source Access

import{HoldMyTask}from"@cldmv/holdmytask/main";// Conditional: uses source files in development, dist files in production

Common Queue System Aliases

For familiarity with other queue systems, several aliases are available:

import{HoldMyTask,// Original class namequeue,// Lower-case alias for common conventionsQueue,// Standard queue namingTaskManager,// Task management systemsTaskQueue,// Task-specific queuingQueueManager,// Queue management systemsTaskProcessor// Task processing systems}from"@cldmv/holdmytask";// All aliases are functionally identicalconstmyQueue=newqueue({concurrency: 5});conststdQueue=newQueue({concurrency: 5});constmanager=newTaskManager({priorities: {high: {delay: 100}}});

Direct Source Import (Development Only)

import{HoldMyTask}from"@cldmv/holdmytask/src";// Always uses source files - bypasses devcheck

Note: The main import automatically runs environment checks in development mode to ensure proper configuration.

🚀 Quick Start

import{HoldMyTask}from"@cldmv/holdmytask";constqueue=newHoldMyTask({concurrency: 2,delays: {1: 100,2: 200},// 100ms delay after priority 1 tasks, 200ms after priority 2coalescingWindowDuration: 200,// Group similar tasks within 200mscoalescingMaxDelay: 1000// Force execution after 1000ms max});// Enqueue a taskqueue.enqueue(async(signal)=>{// Your task logic herereturn"task result";},(error,result)=>{if(error){console.error("Task failed:",error);}else{console.log("Task completed:",result);}},{priority: 1,timeout: 5000});// Coalescing example - multiple similar tasks become oneconstresults=awaitPromise.all([queue.enqueue(async()=>updateUI(),{coalescingKey: "ui.update"}),queue.enqueue(async()=>updateUI(),{coalescingKey: "ui.update"}),queue.enqueue(async()=>updateUI(),{coalescingKey: "ui.update"})]);// Only one updateUI() call executes, all three promises resolve with the same result

🔄 Promise API

For modern async/await codebases, you can omit the callback to get a Promise:

import{HoldMyTask}from"@cldmv/holdmytask";constqueue=newHoldMyTask({concurrency: 2});// Promise-based tasktry{constresult=awaitqueue.enqueue(async(signal)=>{// Task logic with abort supportif(signal.aborted)thrownewError("Aborted");return"task result";},{priority: 1,timeout: 5000});console.log("Task completed:",result);}catch(error){console.error("Task failed:",error.message);}// The returned Promise is also a task handleconstpromise=queue.enqueue(()=>"simple task");console.log("Task ID:",promise.id);promise.cancel();// Cancel the task

Mixing APIs

You can mix callback and promise styles in the same queue:

// Callback-basedqueue.enqueue(task1,callback,options);// Promise-basedconstresult=awaitqueue.enqueue(task2,options);

🏗️ Async Constructor Pattern

HoldMyTask works excellently with async constructor patterns for classes that need initialization:

classAsyncService{constructor(options={}){this.queue=newHoldMyTask({concurrency: options.concurrency||2,maxQueue: -1// Unlimited queue for initialization tasks});this.ready=this.initialize();}asyncinitialize(){// Initialization tasks that need to complete before the service is readyawaitthis.queue.enqueue(async()=>{this.config=awaitthis.loadConfiguration();});awaitthis.queue.enqueue(async()=>{this.database=awaitthis.connectToDatabase();});awaitthis.queue.enqueue(async()=>{this.cache=awaitthis.initializeCache();});returnthis;}asyncprocessData(data){// Ensure service is initialized before processingawaitthis.ready;returnthis.queue.enqueue(async()=>{// Process data using initialized resourcesreturnthis.database.save(awaitthis.transformData(data));});}// Clean shutdownasyncshutdown(){awaitthis.queue.drain();// Wait for all tasks to completeawaitthis.database.close();}}// Usageconstservice=newAsyncService({concurrency: 5});awaitservice.ready;// Wait for initializationconstresult=awaitservice.processData(someData);

This pattern is particularly useful for:

  • Database connections: Initialize connections before accepting work
  • Configuration loading: Load settings before processing tasks
  • Resource acquisition: Set up required resources in a controlled manner
  • Dependency injection: Initialize dependencies in the correct order

📚 API Reference

Constructor

constqueue=newHoldMyTask(options?)

Async Initialization Pattern

For async initialization that allows event listeners to be attached before any initialization events can fire, use sync: false:

// Create instance with async initializationconstqueuePromise=newHoldMyTask({concurrency: 5,maxQueue: 100,sync: false// Enable async initialization});// Attach event listeners before initialization completesqueuePromise.on("error",(err)=>console.error("Queue error:",err));queuePromise.on("warning",(warning)=>console.warn("Warning:",warning.message));// Wait for initialization to completeconstqueue=awaitqueuePromise;

This pattern is particularly useful when you need to handle initialization errors or warnings through event listeners rather than try/catch blocks.

Options:

  • concurrency (number, default: 1) - Maximum concurrent tasks
  • smartScheduling (boolean, default: true) - Use dynamic timeout scheduling for better performance
  • tick (number, default: 25) - Scheduler tick interval in milliseconds (used when smartScheduling is false)
  • autoStart (boolean, default: true) - Whether to start processing immediately
  • defaultPriority (number, default: 0) - Default task priority
  • maxQueue (number, default: Infinity) - Maximum queued tasks. Use -1 for unlimited queue capacity (equivalent to Infinity)
  • delays (object, default: {}) - DEPRECATED: Priority-to-delay mapping for completion delays (use priorities instead)
  • priorities (object, default: {}) - Priority-specific configuration: { [priority]: { concurrency, postDelay, startDelay } }
    • concurrency (number) - Maximum concurrent tasks for this priority (defaults to global concurrency limit)
    • postDelay (number) - Delay after task completion before next task of same priority
    • startDelay (number) - Delay before task execution (pre-execution delay)
  • coalescing (object) - Enhanced coalescing configuration
    • defaults (object) - Default settings for all coalescing keys
      • windowDuration (number, default: 200) - Window duration in milliseconds
      • maxDelay (number, default: 1000) - Maximum delay before forcing execution
      • delay (number) - Default completion delay for coalescing tasks
      • start (number) - Default start delay for coalescing tasks
      • resolveAllPromises (boolean, default: true) - Whether all promises resolve with result
      • multipleCallbacks (boolean, default: false) - Whether to call multiple callbacks
    • keys (object) - Per-key configuration overrides: { [key]: { windowDuration, maxDelay, delay, start, ... } }
  • coalescingWindowDuration (number, default: 200) - DEPRECATED: Use coalescing.defaults.windowDuration
  • coalescingMaxDelay (number, default: 1000) - DEPRECATED: Use coalescing.defaults.maxDelay
  • coalescingResolveAllPromises (boolean, default: true) - DEPRECATED: Use coalescing.defaults.resolveAllPromises
  • onError (function) - Global error handler
  • now (function) - Injectable clock for testing

Methods

enqueue(task, callback?, options?)

Adds a task to the queue.

Parameters:

  • task (function) - The task function that receives an AbortSignal and returns a value or Promise
  • callback (function, optional) - Called with (error, result) when task completes. If omitted, returns a Promise.
  • options (object) - Task options

Task Options:

  • priority (number) - Task priority (higher = more important)
  • delay (number) - Override completion delay for this task (use -1 to bypass delays)
  • bypassDelay (boolean) - If true, skip any active delay period and start immediately
  • timeout (number) - Timeout in milliseconds
  • signal (AbortSignal) - External abort signal
  • timestamp (number) - Absolute execution timestamp
  • start (number) - Milliseconds from now when the task should be ready to run (convenience for timestamp calculation)
  • coalescingKey (string) - Tasks with the same coalescing key can be merged for efficiency
  • mustRunBy (number) - Absolute timestamp by which the task must execute (overrides coalescing delays)
  • metadata (any) - Custom metadata attached to the task. Individual metadata is always directly accessible via the returned task handle

Returns: TaskHandle object with:

  • id - Unique task identifier
  • cancel(reason?) - Cancel the task
  • status() - Get current status
  • startedAt - When task started
  • finishedAt - When task finished
  • result - Task result (if completed)
  • error - Task error (if failed)
  • metadata - Custom metadata attached to this task
  • coalescingInfo - Coalescing group information (null for non-coalesced tasks)

When callback is omitted, returns a Promise that resolves with the task result or rejects with an error.

Accessing Task Metadata

Every task handle provides direct access to its metadata and coalescing information:

// Create a task with metadataconsttask=queue.enqueue(async()=>{return"task completed";},{metadata: {userId: 123,action: "save",document: "report.pdf"},coalescingKey: "user-actions"});// Direct access to metadata - works for both regular and coalesced tasksconsole.log(task.metadata);// Output: { userId: 123, action: 'save', document: 'report.pdf' }// For coalesced tasks, get information about the coalescing groupif(task.coalescingInfo){console.log("Coalescing key:",task.coalescingInfo.coalescingKey);console.log("Group size:",task.coalescingInfo.taskCount);console.log("All task metadata in group:",task.coalescingInfo.allTaskMetadata);}// Works the same way with Promise APIconstresult=awaittask;console.log("Still accessible after completion:",task.metadata);

Key Points:

  • task.metadata - Returns the original metadata you attached to the task
  • task.coalescingInfo - Returns coalescing group details (null for non-coalesced tasks)
  • Available on both callback-style task handles and Promise-style task handles
  • Accessible before, during, and after task execution
  • For coalesced tasks, each task retains its individual metadata

Method Aliases

For compatibility with common queue system naming conventions, the following aliases are available:

  • schedule(task, callback?, options?) - Alias for enqueue()
  • add(task, callback?, options?) - Alias for enqueue()
// These are all equivalent:queue.enqueue(task,options);queue.schedule(task,options);queue.add(task,options);

Custom Task IDs

Tasks can be assigned custom IDs for easier tracking and management:

// Assign custom IDconsttask=queue.enqueue(myTask,{id: "user-action-123"});console.log(task.id);// 'user-action-123'// Task management by IDqueue.has("user-action-123");// trueconsttask=queue.get("user-action-123");// task object or nullqueue.cancel("user-action-123","User cancelled");// returns true if cancelled// Uniqueness is enforcedqueue.enqueue(task1,{id: "duplicate"});queue.enqueue(task2,{id: "duplicate"});// throws: Task ID "duplicate" already exists

ID Management Methods:

  • has(id) - Check if a task with the given ID exists
  • get(id) - Get task object by ID (returns null if not found)
  • cancel(id, reason?) - Cancel task by ID (returns boolean success)

Method Aliases (for backward compatibility):

  • hasTask(id) - Alias for has(id)
  • getTask(id) - Alias for get(id)
  • cancelTask(id, reason?) - Alias for cancel(id, reason?)

Control Methods

  • pause() - Pause task execution
  • resume() - Resume task execution
  • clear() - Cancel all pending tasks
  • size() - Get total queued tasks
  • length() - Alias for size() - get total queued tasks
  • inflight() - Get currently running tasks
  • destroy() - Destroy the queue and cancel all tasks
  • shutdown() - Alias for destroy() - convenient for common queue system naming
  • now() - Get current timestamp (useful for testing with custom clocks)

Configuration Methods

  • configurePriority(priority, config) - Configure or update priority-specific settings
    • priority (string|number) - Priority level to configure
    • config (object) - Configuration: { delay?, maxDelay?, start? }
  • configureCoalescingKey(key, config) - Configure or update coalescing key settings
    • key (string) - Coalescing key to configure
    • config (object) - Configuration: { windowDuration?, maxDelay?, delay?, start?, multipleCallbacks?, resolveAllPromises? }
  • getPriorityConfig(priority, taskOptions?) - Get effective configuration for a specific priority
  • getPriorityConfigurations() - Get all configured priorities and their settings
  • getCoalescingConfig(coalescingKey, taskOptions?) - Get effective configuration for a specific coalescing key
  • getCoalescingConfigurations() - Get all configured coalescing keys and their settings

Coalescing Group Inspection Methods

  • getCoalescingGroup(coalescingKey, groupId?) - Get detailed information about coalescing groups
    • coalescingKey (string) - The coalescing key to query
    • groupId (string, optional) - Specific group ID, or omit to get all groups for the key
    • Returns group info with task details and metadata
  • getCoalescingGroupMetadata(coalescingKey, groupId?) - Get metadata from all tasks in coalescing groups
    • Returns array of { taskId, groupId, metadata } objects
  • getCoalescingGroupsSummary() - Get summary of all active coalescing groups
    • Returns object with { [coalescingKey]: { groupCount, totalTasks } }
  • findCoalescingGroupByTaskId(taskId) - Find which coalescing group contains a specific task
    • Returns group info including the task's metadata and other group members

Note: For most use cases, the direct task.metadata and task.coalescingInfo properties provide easier access to task information. The coalescing group inspection methods are useful for bulk operations, monitoring, debugging, or when you need to query groups by coalescing key without having individual task references.

// Example: Direct access to task metadata and coalescing infoconsttask1=queue.enqueue(async()=>"result",{coalescingKey: "user-action",metadata: {userId: 123,action: "save"}});consttask2=queue.enqueue(async()=>"result",{coalescingKey: "user-action",metadata: {userId: 456,action: "delete"}});// Direct access - no additional API calls needed!console.log("Task 1 metadata:",task1.metadata);// { userId: 123, action: 'save' }console.log("Task 2 metadata:",task2.metadata);// { userId: 456, action: 'delete' }// Access coalescing information directly from the taskconsole.log("Coalescing info:",task1.coalescingInfo);// {// coalescingKey: 'user-action',// groupId: '1',// representativeId: '3',// taskCount: 2,// allTaskMetadata: [// { id: '1', metadata: { userId: 123, action: 'save' }},// { id: '2', metadata: { userId: 456, action: 'delete' }}// ]// }// Advanced: Use inspection methods for bulk operationsconstmetadata=queue.getCoalescingGroupMetadata("user-action");constgroupInfo=queue.getCoalescingGroup("user-action");consttaskGroup=queue.findCoalescingGroupByTaskId(task1.id);

Debug and Inspection Methods

For debugging queue behavior, troubleshooting scheduler issues, or monitoring queue state in production, HoldMyTask provides comprehensive inspection methods:

Quick Debug Output

// Log comprehensive queue state to consolequeue.debugLog();// Basic summaryqueue.debugLog(true);// Detailed task information// Example output:// === HoldMyTask Debug Information ===// Timestamp: 2025-11-14T17:45:32.123Z// Status: ACTIVE// Smart Scheduling: ON// Concurrency: 2/5//// --- Queue State ---// Total Tasks: 7// Pending: 3// Ready: 2// Running: 2//// --- Timer State ---// Interval: None// Timeout: Active (15)// Next Run: 1250ms

Detailed Inspection

// Get comprehensive queue stateconststate=queue.inspect();console.log("Total tasks:",state.totals.totalTasks);console.log("Running tasks:",state.queues.running.tasks);console.log("Next scheduled run:",state.timers.nextRunIn,"ms");// Inspect just the tasksconsttasks=queue.inspectTasks();console.log("Tasks by priority:",tasks.byPriority);console.log("Tasks by coalescing key:",tasks.byCoalescingKey);// Inspect scheduler stateconstscheduler=queue.inspectScheduler();console.log("Can run task now:",scheduler.nextTask?.canRunNow);console.log("Available slots:",scheduler.availableSlots);console.log("Active delays:",scheduler.delays.count);// Inspect timers specificallyconsttimers=queue.inspectTimers();console.log("Scheduler interval active:",timers.schedulerInterval.active);console.log("Next run in:",timers.nextRunIn,"ms");

Inspection Methods

  • inspect() - Get comprehensive queue state including tasks, timers, scheduler, and coalescing information
  • inspectTasks() - Get detailed task information grouped by status, priority, and coalescing key
  • inspectScheduler() - Get scheduler state, timing information, and next task details
  • inspectTimers() - Get timer state and scheduling information
  • debugLog(detailed?) - Log formatted debug information to console

Use Cases:

  • Development: Debug why tasks aren't running or understand queue behavior
  • Production Monitoring: Track queue health and performance metrics
  • Troubleshooting: Identify scheduler issues, timer problems, or concurrency bottlenecks
  • Testing: Verify queue state in automated tests
// Example: Debug scheduler restart issueconstqueue=newHoldMyTask({concurrency: 2});// Add some tasksqueue.enqueue(async()=>"task1");queue.enqueue(async()=>"task2",{priority: 2,start: 5000});// Check initial stateconsole.log("Initial state:");queue.debugLog();// Wait for drainqueue.on("drain",()=>{console.log("After drain:");queue.debugLog();// Add new taskqueue.enqueue(async()=>"task3");console.log("After adding new task:");queue.debugLog();});

Events

queue.on("start",(task)=>{/* task started */});queue.on("success",(task)=>{/* task completed successfully */});queue.on("error",(task)=>{/* task failed */});queue.on("cancel",(task,reason)=>{/* task cancelled */});queue.on("drain",()=>{/* all tasks completed */});queue.on("warning",(warning)=>{/* deprecation or configuration warning */});

Deprecation Warning Events

When deprecated configuration options are used, HoldMyTask emits warning events to help with migration:

constqueue=newHoldMyTask({delays: {1: 100},// Deprecated optioncoalescingWindowDuration: 200// Deprecated option});queue.on("warning",(warning)=>{console.warn(`Deprecation Warning: ${warning.message}`);console.warn(`Use: ${warning.replacement}`);console.warn(`In: ${warning.source}`);});// Output:// Deprecation Warning: Option 'delays' is deprecated// Use: priorities: { 1: { delay: 100 }}// In: constructor options

⚙️ Concurrency & Delays Interaction

Understanding how concurrency and delays work together is crucial for optimal queue behavior:

Concurrency Rules

constqueue=newHoldMyTask({concurrency: 3,// Up to 3 tasks can run simultaneouslydelays: {1: 500,2: 1000}});

Key Behaviors:

  1. Delays are Global: When any task completes, its priority delay affects ALL subsequent task starts
  2. Concurrency Slots: Multiple tasks can run simultaneously until delay blocks new starts
  3. Independent Completion: Running tasks complete independently; delays only affect new starts

Timing Examples

// Timeline with concurrency: 2, delays: { 1: 1000 }// 10:00:00 - Start: TaskA (pri 1), TaskB (pri 1) - both running// 10:00:02 - TaskA completes → 1000ms delay starts, TaskB still running// 10:00:03 - TaskC (pri 1) ready but blocked by delay// 10:00:04 - TaskB completes → extends delay to 10:00:05 (1000ms from TaskB completion)// 10:00:05 - Delay expires, TaskC and TaskD can start (fills 2 slots again)

Best Practices:

  • Use shorter delays with higher concurrency for throughput with gentle rate limiting
  • Use longer delays with lower concurrency for strict rate limiting and resource protection
  • Monitor inflight() to understand how many tasks are actively running vs waiting

⚡ Technical Architecture

HoldMyTask uses a sophisticated dual-heap scheduling system for optimal performance:

🔄 Dual-Heap System:

  • Pending Heap: Time-ordered queue for tasks waiting for their scheduled time (readyAt)
  • Ready Heap: Priority-ordered queue for tasks ready to execute now

📊 Scheduling Flow:

1. New tasks → Pending Heap (ordered by readyAt timestamp)
2. Scheduler tick → Move ready tasks: Pending → Ready Heap
3. Execution → Take highest priority from Ready Heap
4. Completion → Apply delays, emit events, schedule next tick

⏰ Smart Timing:

  • Adaptive scheduling: Uses intervals for immediate tasks, timeouts for distant tasks
  • Precision timing: Sub-millisecond accuracy with injectable clock for testing
  • Efficient scanning: O(log n) heap operations for thousands of tasks

🚀 Performance Characteristics:

  • Enqueue: O(log n) - Insert into priority heap
  • Dequeue: O(log n) - Extract from priority heap
  • Scheduler tick: O(k log n) where k = ready tasks
  • Memory: O(n) - Minimal overhead per task

This architecture enables handling thousands of tasks with precise timing control while maintaining excellent performance.

CodeFactornpms.io score

npm unpacked sizeRepo size

Unlimited Queue Capacity

For scenarios requiring unlimited task queuing, you can set maxQueue to -1:

constqueue=newHoldMyTask({concurrency: 5,maxQueue: -1// Unlimited queue capacity (equivalent to Infinity)});// Now you can enqueue unlimited tasks without hitting queue limitsfor(leti=0;i<100000;i++){queue.enqueue(async()=>processTask(i));}

Use Cases:

  • Batch processing: When processing large datasets where queue size is unpredictable
  • Event-driven systems: Where task volume can spike dramatically
  • Data pipelines: For ETL operations with variable input sizes
  • Development/testing: When you need to stress-test with large task volumes

Memory Considerations:

While -1 allows unlimited queuing, be mindful of memory usage with very large task sets. Each queued task consumes memory until executed.

🎯 Advanced Features

Priority System

Tasks execute in priority order (highest first):

Callback API:

queue.enqueue(task1,callback,{priority: 1});queue.enqueue(task2,callback,{priority: 10});// Runs firstqueue.enqueue(task3,callback,{priority: 5});

Promise API:

awaitqueue.enqueue(task1,{priority: 1});awaitqueue.enqueue(task2,{priority: 10});// Runs firstawaitqueue.enqueue(task3,{priority: 5});

Enhanced Priority & Coalescing Configuration

HoldMyTask supports comprehensive priority and coalescing configuration for sophisticated timing control:

constqueue=newHoldMyTask({concurrency: 3,// Priority-specific defaults (replaces legacy delays)priorities: {1: {delay: 200,start: 0},// High priority: 200ms delay, immediate start2: {delay: 100,start: 25},// Medium priority: 100ms delay, 25ms start delay3: {delay: 50,start: 50}// Low priority: 50ms delay, 50ms start delay},// Enhanced coalescing with per-key settingscoalescing: {defaults: {windowDuration: 200,maxDelay: 1000,delay: 75,// Default completion delay for coalescing tasksstart: 25,// Default start delay for coalescing tasksresolveAllPromises: true},keys: {"ui.update": {windowDuration: 100,maxDelay: 500,delay: 25,// Fast UI updatesstart: 0},"api.batch": {windowDuration: 1000,maxDelay: 5000,delay: 200,// Slower API operationsstart: 100}}}});// Dynamic configurationqueue.configurePriority(4,{delay: 300,start: 75});queue.configureCoalescingKey("data.sync",{windowDuration: 800,maxDelay: 3000,delay: 150,start: 50});// Get configuration informationconstpriority4Config=queue.getPriorityConfig(4);console.log(`Priority 4: ${priority4Config.delay}ms delay, ${priority4Config.start}ms start delay`);constdataSyncConfig=queue.getCoalescingConfig("data.sync");console.log(`Data sync: ${dataSyncConfig.windowDuration}ms window, ${dataSyncConfig.maxDelay}ms max delay`);// Get all configurationsconstallPriorities=queue.getPriorityConfigurations();constallCoalescingKeys=queue.getCoalescingConfigurations();console.log("All priority configs:",allPriorities);console.log("All coalescing configs:",allCoalescingKeys);

Configuration Hierarchy:

  1. Task-level options (highest priority)
  2. Coalescing key configuration
  3. Priority defaults
  4. Coalescing defaults
  5. System defaults (lowest priority)

Backward Compatibility: Legacy delays options are automatically converted to the new priorities format.

Smart Scheduling

By default, HoldMyTask uses intelligent dynamic scheduling that calculates optimal timeout intervals based on when tasks should become ready. This provides significant performance improvements over traditional polling.

Benefits:

  • 31x performance improvement in typical scenarios
  • Precise timing - tasks execute exactly when ready
  • CPU efficient - no constant polling overhead
  • Dynamic adaptation - adjusts to task timing patterns
// Smart scheduling enabled by defaultconstqueue=newHoldMyTask();// Traditional polling mode (for compatibility)constlegacyQueue=newHoldMyTask({smartScheduling: false,tick: 25// polling interval in ms});

How it works:

  • Calculates the next task ready time
  • Sets a precise timeout for that moment
  • Includes healing mechanism to prevent scheduler stalls
  • Falls back gracefully on complex timing scenarios

Per-Priority Concurrency Limits

HoldMyTask supports granular concurrency control at the priority level, allowing you to fine-tune how many tasks of each priority can run simultaneously while still respecting the global concurrency limit.

Key Benefits

  • Database operations - Run only 1 critical database migration at a time
  • API rate limiting - Limit API calls per priority to avoid overwhelming services
  • Resource management - Control expensive operations while allowing lightweight tasks
  • Flexible scaling - Different priorities can have different concurrency characteristics

Configuration

constqueue=newHoldMyTask({concurrency: 8,// Global maximum: 8 total tasks across all prioritiespriorities: {1: {concurrency: 1,// Critical: Only 1 at a time (database migrations, etc.)postDelay: 100,// 100ms delay after completionstartDelay: 0// No pre-execution delay},2: {concurrency: 3,// Important: Up to 3 at a time (API calls, file processing)postDelay: 200,// 200ms delay after completionstartDelay: 50// 50ms pre-execution delay},3: {concurrency: 5,// Background: Up to 5 at a time (cleanup, logging, etc.)postDelay: 0,// No post-completion delaystartDelay: 100// 100ms pre-execution delay}// Priority 4+ tasks: No specific limit, use global concurrency}});

How It Works

  1. Global Limit First - The total running tasks never exceed the global concurrency setting
  2. Per-Priority Limits - Tasks of each priority respect their individual concurrency limit
  3. Dynamic Scheduling - If one priority is at its limit, other priorities can still start tasks
  4. Automatic Fallback - Priorities without concurrency settings use the global limit

Usage Examples

// Database migration (priority 1): Only 1 can run at a timeawaitqueue.enqueue(async()=>{awaitrunDatabaseMigration();},{priority: 1});// API calls (priority 2): Up to 3 can run concurrentlyawaitqueue.enqueue(async()=>{returnawaitfetchUserData(userId);},{priority: 2});// Background cleanup (priority 3): Up to 5 can run concurrentlyawaitqueue.enqueue(async()=>{awaitcleanupTempFiles();},{priority: 3});// High-priority urgent task (priority 10): Uses global concurrency limitawaitqueue.enqueue(async()=>{awaithandleEmergencyAlert();},{priority: 10});

Monitoring Per-Priority Concurrency

// Check current concurrency stateconstinspection=queue.inspect();console.log("Global concurrency:",inspection.scheduler.currentConcurrency+"/"+inspection.scheduler.concurrency);// Per-priority concurrency informationObject.entries(inspection.scheduler.priorityConcurrency).forEach(([priority,info])=>{console.log(`Priority ${priority}: ${info.running}/${info.limit} (${info.available} available)`);});// Or use the scheduler-specific inspectionconstscheduler=queue.inspectScheduler();console.log("Priority concurrency limits:",scheduler.priorityConcurrency);

Example Output

Global concurrency: 6/8
Priority 1: 1/1 (0 available)
Priority 2: 2/3 (1 available)
Priority 3: 3/5 (2 available)

Priority Delays - Advanced Timing Control

Priority delays create "cool-down" periods after task completion based on the completed task's priority. This helps prevent resource overwhelming, implement rate limiting, and control timing between operations.

How It Works

  • When a task completes, the delay for its priority level is enforced
  • ALL subsequent tasks (any priority) must wait for the delay period to expire
  • Delays apply globally - they affect the entire queue, not just tasks of the same priority
  • Tasks can override delays on a per-task basis or bypass delays entirely
constqueue=newHoldMyTask({concurrency: 1,delays: {1: 1000,// 1 second delay after priority 1 tasks complete2: 500,// 500ms delay after priority 2 tasks complete3: 0// No delay after priority 3 tasks (explicit)}});// Task A (priority 1) completes at 10:00:05// → Next task can't start until 10:00:06 (1000ms delay)// Task B (priority 2) completes at 10:00:07// → Next task can't start until 10:00:07.5 (500ms delay)// Override delay for specific taskqueue.enqueue(task,callback,{priority: 1,delay: 200});// Uses 200ms instead of 1000ms// Set zero delay for specific taskqueue.enqueue(task,callback,{priority: 1,delay: 0});// No delay after this task

Delay Bypass - Emergency Task Injection

When urgent tasks need to execute immediately, bypassing active delay periods, use the bypassDelay option or delay: -1 syntax. This is perfect for emergency situations, high-priority interrupts, or critical system tasks.

Bypass Behavior

constqueue=newHoldMyTask({concurrency: 1,delays: {1: 1000}// 1 second delay after priority 1 tasks});// Timeline example:// 10:00:00 - Task A (priority 1) completes → 1000ms delay starts// 10:00:00.1 - Normal task B enqueued → must wait until 10:01:00// 10:00:00.2 - Urgent task C enqueued with bypass → can start now (bypasses delay)queue.enqueue(normalTaskB,callback,{priority: 1});// Waits for delayqueue.enqueue(urgentTaskC,callback,{priority: 1,bypassDelay: true// Skips the 1000ms delay, can start now});// Alternative bypass syntaxqueue.enqueue(emergencyTaskD,callback,{priority: 1,delay: -1// Same as bypassDelay: true});

Advanced Bypass Scenarios

constqueue=newHoldMyTask({concurrency: 2,delays: {1: 800,2: 400}});// Multiple tasks with different bypass behaviorqueue.enqueue(taskA,callback,{priority: 1});// Completes, starts 800ms delayqueue.enqueue(taskB,callback,{priority: 2});// Waits for 800ms delayqueue.enqueue(taskC,callback,{priority: 2,bypassDelay: true});// Bypasses delay, can start nowqueue.enqueue(taskD,callback,{priority: 1});// Waits for taskC's completion delay// Execution order: taskA → taskC (bypass) → taskB → taskD

Critical Notes:

  • Bypass affects START timing only - bypassed tasks still apply their own completion delays
  • Maintains priority ordering - bypass doesn't override natural priority rules
  • Scans entire queue - finds bypass tasks even when normal tasks are blocked
  • Concurrency aware - works correctly with multiple concurrent execution slots
  • ⚠️Use sparingly - frequent bypassing defeats the purpose of delay-based rate limiting

🔄 Task Coalescing System

The coalescing system allows multiple similar tasks to be intelligently merged, reducing redundant operations while ensuring all promises resolve with accurate results. This is perfect for scenarios like UI updates, API calls, or device commands where only the final result matters.

How Coalescing Works

When tasks with the same coalescingKey are enqueued within a time window, they get merged into groups:

  1. First task creates a new coalescing group with a time window
  2. Subsequent tasks with the same key join the existing group (if within the window)
  3. One representative task executes for the entire group
  4. All promises in the group resolve with the same result

⚠️Critical Timing Consideration: Real-world tasks take time to execute (100ms-2000ms+). If your coalescing tasks need to see the final state from other operations, ensure proper timing with start delays or timestamp scheduling. Tasks that start too early may see intermediate states rather than final results.

🎯 Correct Coalescing Pattern: Fire-and-Forget with Embedded Updates

The most reliable pattern for coalescing with state consistency is the "fire-and-forget with embedded updates" approach:

❌ WRONG - Parallel Enqueueing:

// DON'T DO THIS - creates race conditionsfunctionvolumeUp(){constvolumePromise=queue.enqueue(changeVolume,{priority: 1});constupdatePromise=queue.enqueue(updateUI,{coalescingKey: "ui"});// May see stale statereturnPromise.all([volumePromise,updatePromise]);}

✅ CORRECT - Embedded Update Pattern:

// DO THIS - guarantees state consistencyfunctionvolumeUp(){returnqueue.enqueue(async()=>{// 1. Perform the main operationconstresult=awaitchangeVolume();// 2. AFTER main operation completes, enqueue the update FROM WITHINqueue.enqueue(async()=>updateUI(),{coalescingKey: "ui"});// Always sees final statereturnresult;// Consumer gets immediate response},{priority: 1});}

Key Benefits:

  • 100% State Accuracy: Updates only trigger after main operations complete
  • Fire-and-Forget: Consumers get immediate responses, no waiting
  • Optimal Coalescing: Multiple updates coalesce naturally when triggered close together
  • No Race Conditions: Sequential execution guarantees consistent state

Basic Coalescing Configuration

constqueue=newHoldMyTask({concurrency: 1,coalescingWindowDuration: 200,// 200ms window for grouping taskscoalescingMaxDelay: 1000,// Maximum 1000ms delay before forcing executioncoalescingResolveAllPromises: true// All promises get the result (default: true)});

Simple Coalescing Example

// Device volume control scenarioasyncfunctionupdateVolume(change){returnqueue.enqueue(async()=>{// Realistic device operations take timeconstcurrentVolume=awaitdevice.getVolume();// ~50-200msconstnewVolume=Math.max(0,Math.min(100,currentVolume+change));awaitdevice.setVolume(newVolume);// ~100-500msreturnnewVolume;},{coalescingKey: "volume.update",// Tasks with same key get groupedpriority: 1,delay: 100// 100ms delay after completion});}// User rapidly presses volume up 5 times within coalescing windowconstresults=awaitPromise.all([updateVolume(1),// Creates new groupupdateVolume(1),// Joins existing group (if within 200ms window)updateVolume(1),// Joins existing group (if within 200ms window)updateVolume(1),// Joins existing group (if within 200ms window)updateVolume(1)// Joins existing group (if within 200ms window)]);// If all tasks coalesce: ONE device.setVolume() call with final state// If timing spreads them: Multiple groups, each sees state at execution timeconsole.log(results);// Could be [55, 55, 55, 55, 55] or [51, 53, 55, 55, 55] depending on timing

Advanced Coalescing with Fire-and-Forget Pattern

The correct pattern for coalescing with updates is to enqueue update tasks from within the main tasks after they complete. This ensures 100% accuracy and proper state consistency:

constqueue=newHoldMyTask({concurrency: 2,// Allow volume and update tasks to run concurrentlycoalescingWindowDuration: 200,coalescingMaxDelay: 1000});// Volume commands using fire-and-forget patternfunctionvolumeUp(amount=1){constcommandId=Date.now();// Fire-and-forget: return promise but consumer doesn't await itreturnqueue.enqueue(async()=>{console.log(`Executing volume command ${commandId}`);// Realistic device operation takes timeconstresult=awaitdevice.increaseVolume(amount);// 200-1000ms// AFTER volume completes, enqueue update task FROM WITHINconsole.log(`Volume complete, triggering UI update`);queue.enqueue(async()=>{console.log(`Updating UI for final volume`);// UI operation sees accurate final stateconstvolume=awaitdevice.getVolume();// ~50msupdateVolumeDisplay(volume);// ~10-100msreturnvolume;},{coalescingKey: "volume.ui.update",// UI updates get coalescedpriority: 5,// Lower priority than volume commandsdelay: 100// Brief delay after UI updates});returnresult;},{priority: 1,// High priority for user actionsdelay: 100// Brief delay after volume operations});}// Consumer usage - fire and forgetvolumeUp(1);// Optimistic: assume it will work, don't waitvolumeUp(1);// Multiple rapid callsvolumeUp(1);// Each triggers its own update after completingvolumeUp(1);// Updates get coalesced if close togethervolumeUp(1);// Final state is always accurate// Result: 5 volume commands execute sequentially// Update commands coalesce (e.g., 5 → 3 actual updates)// UI always shows accurate final state because updates only trigger after volume changes// Consumer gets immediate response, no waiting for completion

Multi-Group Coalescing

The same coalescingKey can have multiple active groups based on timing windows:

// API batch processingasyncfunctionprocessDataBatch(data){returnqueue.enqueue(async()=>{console.log(`Processing batch with ${data.length} items`);returnawaitapi.processBatch(data);},{coalescingKey: "api.batch.process",priority: 2,start: 100// 100ms delay allows grouping});}// Timeline:// 0ms: processDataBatch(data1) → creates Group A (window: 0-200ms)// 50ms: processDataBatch(data2) → joins Group A// 250ms: processDataBatch(data3) → creates Group B (window: 250-450ms)// 300ms: processDataBatch(data4) → joins Group B// 500ms: processDataBatch(data5) → creates Group C (window: 500-700ms)// Result: 3 separate API calls, each processing coalesced batches

Coalescing with Explicit Timestamps

For precise scheduling, use timestamp instead of start:

constqueue=newHoldMyTask({coalescingWindowDuration: 300,coalescingMaxDelay: 2000});// Schedule all updates for the same exact timeconstscheduleTime=Date.now()+1000;// 1 second from nowconstpromises=[];for(leti=0;i<10;i++){promises.push(queue.enqueue(async()=>{console.log("Executing coalesced batch update");returnawaitperformBatchUpdate();},{coalescingKey: "batch.update",timestamp: scheduleTime,// All scheduled for same timepriority: 3}));}// All 10 tasks coalesce into 1 execution at exactly scheduleTimeconstresults=awaitPromise.all(promises);console.log(`10 tasks became 1 execution, all got result:`,results[0]);

Must-Run-By Deadlines

Control maximum delay with mustRunBy to ensure tasks don't wait too long:

constqueue=newHoldMyTask({coalescingWindowDuration: 500,// Try to group for 500mscoalescingMaxDelay: 2000// But never wait more than 2 seconds});// Critical system updatesasyncfunctioncriticalSystemUpdate(updateData){returnqueue.enqueue(async()=>{console.log("Executing critical system update");returnawaitapplySystemUpdate(updateData);},{coalescingKey: "system.critical.update",priority: 1,timestamp: Date.now()+300,// Prefer 300ms delaymustRunBy: Date.now()+1500// Must run within 1.5 seconds});}// Even if coalescing window suggests waiting longer,// the task will execute by the mustRunBy deadline

Promise Resolution Modes

Control how promises resolve within coalescing groups:

constqueue=newHoldMyTask({coalescingResolveAllPromises: true,// Default: all promises get the resultcoalescingWindowDuration: 200});// Mode 1: All promises resolve (default behavior)constresults1=awaitPromise.all([queue.enqueue(task,{coalescingKey: "test"}),queue.enqueue(task,{coalescingKey: "test"}),queue.enqueue(task,{coalescingKey: "test"})]);// All three promises resolve with the same result// Mode 2: Only representative promise resolvesconstqueue2=newHoldMyTask({coalescingResolveAllPromises: false,coalescingWindowDuration: 200});const[result1,result2,result3]=awaitPromise.all([queue2.enqueue(task,{coalescingKey: "test"}),// Resolves with resultqueue2.enqueue(task,{coalescingKey: "test"}),// Resolves with undefinedqueue2.enqueue(task,{coalescingKey: "test"})// Resolves with undefined]);// Only the first (representative) promise gets the actual result

Real-World Coalescing Patterns

Device Control Pattern

classVolumeController{constructor(){this.queue=newHoldMyTask({concurrency: 2,// Allow volume and update tasks concurrentlycoalescingWindowDuration: 200,coalescingMaxDelay: 1000});}volumeUp(amount=1){// Fire-and-forget pattern: consumer gets immediate responsereturnthis.queue.enqueue(async()=>{// Realistic device operations take timeconstcurrentVolume=awaitthis.device.getVolume();// ~50-200msconstnewVolume=Math.min(100,currentVolume+amount);awaitthis.device.setVolume(newVolume);// ~100-500ms// AFTER volume change completes, enqueue UI update FROM WITHINthis.queue.enqueue(async()=>{constvolume=awaitthis.device.getVolume();// ~50msthis.ui.updateVolumeDisplay(volume);// ~10-100msreturnvolume;},{coalescingKey: "volume.ui.update",// Updates coalesce togetherpriority: 3,// Lower priority than volume changesdelay: 100// Brief delay after UI updates});returnnewVolume;},{priority: 1,// High priority for user actionsdelay: 50// Brief delay between volume operations});}}

API Batch Processing Pattern

classAPIBatcher{constructor(){this.queue=newHoldMyTask({concurrency: 2,coalescingWindowDuration: 300,coalescingMaxDelay: 1500});}asyncsubmitData(data){returnthis.queue.enqueue(async()=>{// All data submissions in the time window get batchedconstbatchData=this.collectBatchData();// Implementation detailconstresult=awaitthis.api.submitBatch(batchData);returnresult.find((item)=>item.id===data.id);},{coalescingKey: `batch.${data.category}`,// Batch by categorypriority: 2,metadata: { data,category: data.category}});}}

Coalescing Performance Benefits

Real-world performance improvements with the embedded update pattern:

// Fire-and-forget pattern with embedded coalescing updatesconstqueue=newHoldMyTask({concurrency: 2,coalescingWindowDuration: 200,coalescingMaxDelay: 1000});// Volume control example - realistic embedded patternfunctionprocessVolumeCommands(){// Consumer fires 100 rapid volume commands (fire-and-forget)constpromises=[];for(leti=0;i<100;i++){promises.push(queue.enqueue(async()=>{// Main operation: change volume (200-500ms each)constresult=awaitdevice.changeVolume(1);// Embedded update: triggered AFTER volume change (coalesces automatically)queue.enqueue(async()=>updateVolumeDisplay(),// 50-100ms each{coalescingKey: "volume.display"});returnresult;// Immediate response to consumer},{priority: 1}));}// Consumer gets immediate responses, doesn't wait for display updatesreturnpromises;// All resolve quickly with volume results}// Results with embedded coalescing:// - 100 volume operations execute (necessary work)// - ~5-15 display updates execute (95-85% coalescing efficiency)// - 100% accuracy: displays always show final state// - Fire-and-forget: consumers get immediate responses// - Total time: ~25-30 seconds (vs 50+ seconds without coalescing)

Timeouts

Tasks automatically timeout and either call the callback with an error or reject the promise:

Callback API:

queue.enqueue(async(signal)=>{// Long-running taskawaitsomeAsyncOperation();return"result";},(error,result)=>{if(error?.type==="timeout"){console.log("Task timed out!");}},{timeout: 5000}// 5 second timeout);

Promise API:

try{constresult=awaitqueue.enqueue(async(signal)=>{// Long-running taskawaitsomeAsyncOperation();return"result";},{timeout: 5000}// 5 second timeout);console.log("Task completed:",result);}catch(error){if(error.message.includes("timed out")){console.log("Task timed out!");}}

🛑 AbortController Support

The library uses AbortController for cooperative task cancellation. This allows tasks to be cancelled gracefully without forcing termination.

How It Works

  1. Timeout Cancellation: When a task exceeds its timeout, an AbortController is aborted
  2. External Cancellation: You can pass your own AbortSignal to cancel tasks
  3. Cooperative: Tasks must check the signal and respond to cancellation

Implementing AbortController Support in Tasks

Your task functions receive an AbortSignal as the first parameter. Always check this signal to support cancellation:

Callback API:

// ❌ Bad - doesn't support cancellationqueue.enqueue(async()=>{awaitfetch("https://api.example.com/data");// Can't be cancelledreturn"result";},callback);// ✅ Good - supports cancellationqueue.enqueue(async(signal)=>{constresponse=awaitfetch("https://api.example.com/data",{
signal // Pass the signal to fetch});returnresponse.json();},callback);

Promise API:

// ❌ Bad - doesn't support cancellationconstresult=awaitqueue.enqueue(async()=>{awaitfetch("https://api.example.com/data");// Can't be cancelledreturn"result";});// ✅ Good - supports cancellationconstresult=awaitqueue.enqueue(async(signal)=>{constresponse=awaitfetch("https://api.example.com/data",{
signal // Pass the signal to fetch});returnresponse.json();});

Checking the Signal Manually

For custom cancellation logic:

Callback API:

queue.enqueue(async(signal)=>{// Check signal at operation pointsif(signal.aborted){thrownewError("Task was cancelled");}constresult1=awaitsomeOperation();if(signal.aborted){thrownewError("Task was cancelled");}constresult2=awaitanotherOperation();return{ result1, result2 };},callback,{timeout: 10000});

Promise API:

try{constresult=awaitqueue.enqueue(async(signal)=>{// Check signal at operation pointsif(signal.aborted){thrownewError("Task was cancelled");}constresult1=awaitsomeOperation();if(signal.aborted){thrownewError("Task was cancelled");}constresult2=awaitanotherOperation();return{ result1, result2 };},{timeout: 10000});console.log("Task completed:",result);}catch(error){console.log("Task failed:",error.message);}

External Cancellation

Cancel tasks using your own AbortController:

Callback API:

constcontroller=newAbortController();queue.enqueue(task,callback,{signal: controller.signal,timeout: 5000});// Cancel the task externallycontroller.abort();

Promise API:

constcontroller=newAbortController();constpromise=queue.enqueue(task,{signal: controller.signal,timeout: 5000});// Cancel the task externallycontroller.abort();try{constresult=awaitpromise;console.log("Task completed:",result);}catch(error){console.log("Task cancelled:",error.message);}

Best Practices

  1. Always accept the signal parameter in your task functions
  2. Pass the signal to async operations that support it (fetch, timers, etc.)
  3. Check signal.aborted at logical breakpoints in long-running tasks
  4. Throw errors when detecting cancellation
  5. Use descriptive error messages for cancelled tasks

Limitations

  • Single-threaded: Cannot forcibly terminate synchronous JavaScript code
  • Cooperative: Tasks must actively check and respond to the abort signal
  • Async operations: Only cancellable if the underlying operation supports AbortSignal

📝 Examples

Basic Usage

import{HoldMyTask}from"@cldmv/holdmytask";constqueue=newHoldMyTask({concurrency: 3});functionprocessUser(userId){returnqueue.enqueue(async(signal)=>{constuser=awaitfetchUser(userId,{ signal });awaitsendEmail(user.email,"Welcome!",{ signal });returnuser;},(error,user)=>{if(error){console.error(`Failed to process user ${userId}:`,error);}else{console.log(`Processed user: ${user.name}`);}},{priority: 1,timeout: 30000});}

Priority Queue with Delays

constqueue=newHoldMyTask({concurrency: 1,delays: {1: 1000,// 1 second between high-priority tasks2: 100,// 100ms between medium-priority tasks3: 0// No delay for low-priority tasks}});// High priority - runs immediately, 1s delay afterqueue.enqueue(highPriorityTask,callback,{priority: 1});// Medium priority - waits for high priority delay, then 100ms between thesequeue.enqueue(mediumTask1,callback,{priority: 2});queue.enqueue(mediumTask2,callback,{priority: 2});

Batch Processing with Timeouts

constqueue=newHoldMyTask({concurrency: 5,delays: {1: 50}// Small delay between batches});asyncfunctionprocessBatch(items){constresults=[];for(constitemofitems){queue.enqueue(async(signal)=>{returnawaitprocessItem(item,{ signal });},(error,result)=>{if(error){console.error(`Item ${item.id} failed:`,error);}else{results.push(result);}},{priority: 1,timeout: 10000});}// Wait for all to completeawaitnewPromise((resolve)=>queue.on("drain",resolve));returnresults;}

Emergency Task Management with Delay Bypass

Real-world scenario: API rate limiting with emergency override capability.

constapiQueue=newHoldMyTask({concurrency: 2,delays: {1: 2000,// 2 second delay between API calls (rate limiting)2: 5000,// 5 second delay for heavy operations9: 0// No delay for monitoring tasks}});// Regular API operationsapiQueue.enqueue(fetchUserData,handleResponse,{priority: 1});apiQueue.enqueue(syncDatabase,handleResponse,{priority: 2});// System monitoring (high priority, no delays)apiQueue.enqueue(healthCheck,handleResponse,{priority: 9});// EMERGENCY: Critical security alert needs immediate processing// This bypasses any active delays and runs immediatelyapiQueue.enqueue(processSecurityAlert,handleEmergency,{priority: 1,bypassDelay: true,// Skip delay, run NOWtimeout: 10000,// 10 second timeout for critical taskmetadata: {urgency: "critical",alertId: "SEC-001"}});// Alternative syntax for bypassapiQueue.enqueue(emergencyShutdown,handleEmergency,{priority: 1,delay: -1,// Same as bypassDelay: truemetadata: {action: "shutdown"}});functionhandleEmergency(error,result){if(error){console.error("Emergency task failed:",error);// Implement fallback procedures}else{console.log("Emergency handled:",result);// Log to incident management system}}// Monitor queue statussetInterval(()=>{console.log(`Queue: ${apiQueue.size()} total, ${apiQueue.inflight()} running`);},1000);

🧪 Testing & Development

# Run tests
npm test# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:run
# Lint code
npm run lint
# Build for publishing
npm run build

Testing with Injectable Clock

For testing, you can inject a custom clock function and use the now() method to get the current time according to your queue:

letmockTime=Date.now();constqueue=newHoldMyTask({now: ()=>mockTime// Control time in tests});// In tests, you can advance timemockTime+=1000;// Advance by 1 second// The queue's now() method respects your custom clockconsole.log(queue.now());// Returns mockTime valueconsole.log(Date.now());// Returns actual system time// Useful for testing time-based behaviorqueue.enqueue(task,{timestamp: queue.now()+5000});// 5 seconds from mock time

📄 License

GitHub licensenpm license

Apache License 2.0 - see LICENSE file for details.

🤝 Contributing

Contributions welcome! Please ensure:

  • All tests pass
  • Code follows existing style
  • New features include tests
  • Documentation is updated

Made with ❤️ for robust task management

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages