Skip to content

nDriaDev/futurable

Repository files navigation


Futurable Logo

Futurable

The async library JavaScript was missing πŸš€

npm version npm bundle size npm downloads license

coverage statements coverage branches coverage functions coverage lines


🎯 Why Futurable?

JavaScript's async ecosystem has evolved dramatically over the yearsβ€”from callbacks to Promises, from async/await to various control flow libraries. Yet, despite this evolution, critical gaps remain in how we handle asynchronous operations in production applications.

The Problem

Modern applications need more than just Promises. They need:

  • Cancellation: Stop long-running operations when they're no longer needed
  • Composition: Build complex async workflows without callback hell or try-catch pyramids
  • Control: Fine-grained management of concurrency, retries, timeouts, and fallbacks
  • Safety: Handle errors explicitly without littering code with try-catch blocks
  • Reusability: Define async operations once, execute them multiple times

JavaScript's native Promise API offers none of these. AbortController exists but requires verbose boilerplate. Third-party solutions are either too opaque (RxJS), too heavy, or too limited in scope.

The Solution

Futurable fills this gap with two complementary primitives:

  1. Futurable: A Promise with superpowers β€” cancellable, chainable, and resource-aware
  2. FuturableTask: A lazy computation model for functional async composition

Together, they provide everything you need to write robust, maintainable, production-ready async code.


πŸ“– What is Futurable?

Futurable: Cancellable Promises

Futurable extends the native Promise API with built-in cancellation support. It's a drop-in replacement for Promise that solves the resource management problem.

The core insight: When you navigate away from a page, close a modal, or change a filter, you don't just want to ignore pending operations β€” you want to actively stop them and clean up resources.

import { Futurable } from '@ndriadev/futurable';

// Create a cancellable fetch request
const request = Futurable.fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data));

// User navigates away? Cancel it.
request.cancel();

Why this matters:

  • Memory leaks: Prevented by cancelling pending operations
  • Race conditions: Eliminated by cancelling stale requests
  • Resource management: WebSocket connections, timers, and event listeners properly cleaned up
  • User experience: No more stale data updates after navigation

When to use Futurable

Use Futurable when you need immediate execution with cancellation support:

  • React/Vue component effects that need cleanup
  • API requests that should be cancellable
  • Any Promise-based code where you might need to cancel
  • Drop-in replacement for existing Promise code

🎯 What is FuturableTask?

FuturableTask: Lazy Async Composition

FuturableTask represents a blueprint for async work β€” it doesn't execute until you explicitly run it. Think of it as a recipe: you write it once, then execute it multiple times independently.

The core insight: Many async operations benefit from lazy evaluation β€” separating the definition of work from its execution enables powerful patterns like retry, memoization, and functional composition.

import { FuturableTask } from '@ndriadev/futurable';

// Define the work (doesn't execute yet)
const fetchUser = FuturableTask
  .fetch('/api/user')
  .map(res => res.json())
  .retry(3)
  .timeout(5000)
  .memoize();

// Execute when needed
const user = await fetchUser.run();

// Execute again (uses memoized result)
const sameUser = await fetchUser.run();

Why this matters:

  • Reusability: Define once, execute many times
  • Composition: Chain transformations before execution
  • Testing: Easy to test without side effects at definition time
  • Optimization: Memoization, batching, and deduplication
  • Declarative: Describe what should happen, not when

When to use FuturableTask

Use FuturableTask when you need lazy evaluation with advanced composition:

  • Building reusable async workflows
  • Complex pipelines with retry/timeout/fallback logic
  • Operations that should be memoized or deduplicated
  • Functional programming patterns in async code
  • Rate-limited or batched API calls

πŸš€ Core Capabilities

For Futurable

Cancellation

Stop operations and clean up resources. When cancelled, a Futurable stops silently β€” it neither resolves nor rejects. Use onCancel() to react to cancellation:

const request = Futurable.fetch('/api/data')
  .then(res => res.json())
  .onCancel(() => {
    console.log('Cleanup: close connections, clear timers');
  });

// Cancel anytime β€” onCancel callback fires, then/catch are not called
request.cancel();

Built-in Utilities

Native support for common patterns:

// Sleep
await Futurable.sleep({ timer: 1000 });

// Delayed execution
const result = await Futurable.resolve('value')
  .delay(val => val.toUpperCase(), 2000);

// Polling
const controller = Futurable.polling(
  () => checkStatus(),
  { interval: 1000, immediate: true }
);
// Stop later
controller.cancel();

// Cancellable fetch
const data = await Futurable.fetch('/api/data')
  .then(res => res.json());

Safe Error Handling

Handle errors without try-catch:

const result = await Futurable.fetch('/api/data')
  .then(res => res.json())
  .safe();

if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

Unified Sync/Async Entry Point

Wrap any function β€” synchronous or asynchronous, throwing or returning β€” into a Futurable safely:

// Catches synchronous throws that Futurable.resolve(fn()) would miss
const parsed = await Futurable.try(() => JSON.parse(rawInput))
  .safe();

// Works equally well with async functions
const data = await Futurable.try(async () => {
  const res = await fetch('/api/data');
  return res.json();
});

// Unify sync and async callbacks in one interface
function execute(action: () => unknown) {
  return Futurable.try(action)
    .catch(err => console.error('Caught:', err));
}

For FuturableTask

Functional Composition

Build complex pipelines declaratively:

const pipeline = FuturableTask
  .fetch('/api/users')
  .map(res => res.json())
  .map(users => users.filter(u => u.active))
  .map(users => users.sort((a, b) => a.name.localeCompare(b.name)))
  .tap(users => console.log(`Found ${users.length} active users`));

const users = await pipeline.run();

Error Recovery

Sophisticated error handling strategies:

const resilient = FuturableTask
  .fetch('/api/data')
  .timeout(5000)
  .retry(3, 1000)                                          // retry 3 times, 1s delay
  .orElse(() => FuturableTask.fetch('/api/backup'))        // fallback to backup
  .fallbackTo(CACHED_DATA);                                // static fallback value

Concurrency Control

Fine-grained control over parallel execution:

// Limit concurrent requests
const limiter = FuturableTask.createLimiter(5, {
  onActive: () => console.log('Task started'),
  onIdle:   () => console.log('All done')
});

const tasks = urls.map(url =>
  limiter(FuturableTask.fetch(url))
);

// Only 5 run at once
const results = await FuturableTask.parallel(tasks).run();

Debouncing

Automatic debouncing for user input:

const searchTask = FuturableTask
  .of(async (utils) => {
    const res = await utils.fetch('/api/search?q=' + currentQuery);
    return res.json();
  })
  .debounce(300);

// Rapid calls β€” only the last one executes after 300ms
searchTask.run(); // cancelled
searchTask.run(); // cancelled
searchTask.run(); // executes after 300ms

Memoization

Cache expensive operations:

const loadConfig = FuturableTask
  .fetch('/api/config')
  .map(res => res.json())
  .memoize();

const config1 = await loadConfig.run(); // Fetches
const config2 = await loadConfig.run(); // Cached
const config3 = await loadConfig.run(); // Cached

Lazy Sync/Async Entry Point

Create a lazy task from any function β€” sync or async β€” with automatic error capture:

// Sync function that may throw β€” error captured lazily on run()
const parseTask = FuturableTask.try(() => JSON.parse(rawInput));

// Compose freely before executing
const result = await parseTask
  .map(data => data.value)
  .retry(2)
  .runSafe();

if (result.success) {
  console.log('Parsed value:', result.data);
} else {
  console.error('Failed:', result.error);
}

πŸ’‘ Real-World Examples

React Component with Cleanup

import { useEffect, useState } from 'react';
import { Futurable } from '@ndriadev/futurable';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);

    const request = Futurable
      .fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        // Only called for genuine errors, not cancellation
        setError(err);
        setLoading(false);
      })
      .onCancel(() => {
        // Called when the component unmounts or userId changes
        setLoading(false);
      });

    // Cleanup: cancel on unmount or userId change
    return () => request.cancel();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error)   return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

Reusable API Client

class APIClient {
  private baseURL = 'https://api.example.com';

  fetchUser = (id: number) =>
    FuturableTask
      .fetch(`${this.baseURL}/users/${id}`)
      .map(res => res.json())
      .retry(3, 500)
      .timeout(5000)
      .memoize();

  searchUsers = (query: string) =>
    FuturableTask
      .fetch(`${this.baseURL}/users/search?q=${query}`)
      .map(res => res.json())
      .timeout(10000);

  async getUser(id: number) {
    return this.fetchUser(id).run();
  }

  async search(query: string) {
    return this.searchUsers(query).run();
  }
}

Complex Data Pipeline

const processData = FuturableTask
  .fetch('/api/raw-data')
  .map(res => res.json())
  .tap(data => console.log(`Received ${data.length} items`))
  .map(data => data.filter(item => item.active))
  .map(data => data.map(item => ({
    ...item,
    processed: true,
    timestamp: Date.now()
  })))
  .flatMap(data =>
    FuturableTask.traverse(
      data,
      item => FuturableTask.of(() => enrichItem(item))
    )
  )
  .tap(results => console.log(`Processed ${results.length} items`))
  .retry(2, 1000)
  .timeout(30000)
  .catchError(error => {
    console.error('Pipeline failed:', error);
    return FuturableTask.resolve([]);
  });

const results = await processData.run();

Rate-Limited Batch Processing

async function processLargeDataset(items: Item[]) {
  const limiter = FuturableTask.createLimiter(10, {
    onActive:    () => console.log(`Active: ${limiter.activeCount}/10`),
    onCompleted: (result) => updateProgress(result),
    onIdle:      () => console.log('Batch complete')
  });

  const batches = chunk(items, 50);

  const results = await FuturableTask.sequence(
    batches.map(batch =>
      FuturableTask.parallel(
        batch.map(item =>
          limiter(
            FuturableTask
              .of(() => processItem(item))
              .retry(3, 500)
              .timeout(5000)
          )
        )
      )
    )
  ).run();

  return results.flat();
}

🎨 Design Philosophy

1. Progressive Enhancement

Start simple, add complexity only when needed:

// Simple
const data = await Futurable.fetch('/api/data')
  .then(res => res.json());

// Add cancellation
const request = Futurable.fetch('/api/data')
  .then(res => res.json());
request.cancel();

// Add retry and timeout
const resilient = FuturableTask
  .fetch('/api/data')
  .map(res => res.json())
  .retry(3, 500)
  .timeout(5000);

2. Type Safety First

Full TypeScript support with inference:

const result = await FuturableTask
  .of(() => 42)               // FuturableTask<number>
  .map(x => x.toString())     // FuturableTask<string>
  .map(s => s.length)         // FuturableTask<number>
  .run();                     // Promise<number>

3. Zero Dependencies

No external dependencies. Small bundle size. Tree-shakeable.

4. Promise Compatible

Futurable is a Promise. Works with async/await, Promise.all(), and any Promise-based API.


πŸ“¦ Installation

npm install @ndriadev/futurable
yarn add @ndriadev/futurable
pnpm add @ndriadev/futurable

🎯 Quick Start

Basic Futurable

import { Futurable } from '@ndriadev/futurable';

// Cancellable fetch
const request = Futurable.fetch('/api/data')
  .then(res => res.json());

request.cancel(); // Cancel if needed

Basic FuturableTask

import { FuturableTask } from '@ndriadev/futurable';

// Define work
const task = FuturableTask
  .of(() => fetch('/api/data'))
  .map(res => res.json())
  .retry(3, 500);

// Execute when ready
const data = await task.run();

πŸ“š Documentation

πŸ“– Complete Documentation


🌟 Key Features

Futurable

Feature Description
βœ… Cancellation Cancel operations and cleanup resources via onCancel()
βœ… Promise Compatible Drop-in Promise replacement
βœ… Built-in Fetch Cancellable HTTP requests
βœ… Delays & Sleep Timing utilities
βœ… Polling Repeated execution with cancellation control
βœ… Safe Mode Error handling without try-catch via .safe()
βœ… try() Unified sync/async entry point with error capture
βœ… Full TypeScript Complete type safety

FuturableTask

Feature Description
βœ… Lazy Evaluation Define once, execute when needed
βœ… Reusability Run the same task multiple times independently
βœ… Functional Composition map, flatMap, tap, fold, zip, and more
βœ… Retry Logic Configurable retries with optional fixed delay
βœ… Timeout Protection Automatic timeouts with custom reason
βœ… Error Recovery catchError, orElse, fallbackTo
βœ… Concurrency Control Rate limiting and parallelism
βœ… Debouncing & Throttling Built-in debounce and throttle
βœ… Memoization Cache expensive operations
βœ… try() Lazy sync/async entry point with automatic error capture
βœ… Full TypeScript Complete type inference

🎯 Use Cases

Perfect For

  • SPA Applications: Cancel API calls on navigation
  • React/Vue/Angular: Component cleanup and effects
  • Real-time Features: Polling with cancellation
  • Data Processing: Complex async pipelines
  • API Clients: Reusable, composable requests
  • Rate Limiting: Control concurrent operations
  • Form Handling: Debounced search and auto-save
  • Resource Management: Proper async cleanup

🌐 Browser & Node.js Support

  • βœ… All modern browsers (Chrome, Firefox, Safari, Edge)
  • βœ… Node.js 14+
  • βœ… TypeScript 4.5+
  • βœ… ES2015+ (ES6+)

πŸ“„ License

MIT Β© nDriaDev


πŸ™ Acknowledgments

Futurable draws inspiration from:

  • Promises/A+ specification
  • RxJS observables and operators
  • Fluture and functional programming patterns
  • Real-world production challenges in modern web apps

πŸ“ž Support


If you find Futurable useful, please consider giving it a ⭐ on GitHub!

About

futurable

Resources

License

Stars

0 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors