Skip to content

FuturableTask: Lazy Computation

FuturableTask is a powerful abstraction for lazy async computation in Futurable. While Futurable extends Promise and executes eagerly, FuturableTask provides lazy evaluation with functional composition capabilities.

What is FuturableTask?

FuturableTask is a lazy computation wrapper that represents an async operation that hasn't executed yet. Think of it as a recipe or blueprint that defines what should happen, but doesn't actually cook the meal until you tell it to.

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

// Creating a task doesn't execute it
const task = FuturableTask.of(() => {
  console.log('Executing!');
  return fetch('/api/data');
});
// Nothing logged yet, no fetch made

// Execution happens only when you call run()
const result = await task.run(); // Now it executes

Key Differences: Futurable vs FuturableTask

FeatureFuturableFuturableTask
ExecutionEager (immediate)Lazy (on demand)
ReusabilitySingle useMultiple independent runs
CompositionVia .then() chainsVia functional methods
Side EffectsImmediate on creationOnly on .run()
BaseExtends PromiseIndependent class

When to use Futurable?

Use Futurable when you want Promise-like behavior with cancellation:

  • Drop-in Promise replacement
  • React/Vue effects cleanup
  • Immediate execution needed
  • Working with Promise APIs

When to use FuturableTask?

Use FuturableTask when you need:

  • Lazy evaluation
  • Multiple executions of the same operation
  • Complex functional composition
  • Advanced patterns (retry, debounce, memoization)
  • Concurrency control

Core Philosophy

Lazy Evaluation

Tasks don't execute until you call .run():

typescript
// Define what should happen (no execution)
const fetchUsers = FuturableTask
  .of(() => fetch('/api/users'))
  .map(res => res.json())
  .map(users => users.filter(u => u.active));

// Execute when needed
const users = await fetchUsers.run();

// Execute again (independent run)
const moreUsers = await fetchUsers.run();

Functional Composition

Build complex pipelines with pure functions:

typescript
const pipeline = FuturableTask
  .of(() => readFile('data.json'))
  .map(JSON.parse)
  .map(data => data.items)
  .filter(items => items.length > 0)
  .map(items => items.map(processItem))
  .retry(3)
  .timeout(5000);

// All transformations happen during execution
const result = await pipeline.run();

Separation of Definition and Execution

Define your logic once, execute when and where needed:

typescript
// Define reusable tasks
const fetchUserById = (id: number) =>
  FuturableTask
    .of(() => fetch(`/api/users/${id}`))
    .map(res => res.json())
    .retry(3)
    .timeout(5000);

// Use in different contexts
async function loadUserProfile(userId: number) {
  const user = await fetchUserById(userId).run();
  return user;
}

async function loadMultipleUsers(ids: number[]) {
  const tasks = ids.map(fetchUserById);
  const users = await FuturableTask.parallel(tasks).run();
  return users;
}

Basic Concepts

Creating Tasks

Multiple ways to create a FuturableTask:

typescript
// From a sync function
const task1 = FuturableTask.of(() => 42);

// From an async function
const task2 = FuturableTask.of(async () => {
  const res = await fetch('/api/data');
  return res.json();
});

// From a Futurable
const futurable = Futurable.fetch('/api/data');
const task3 = FuturableTask.from(futurable);

// From the constructor
const task4 = new FuturableTask((resolve, reject, utils) => {
  setTimeout(() => resolve('done'), 1000);
});

// Static constructors
const task5 = FuturableTask.resolve('value');
const task6 = FuturableTask.reject(new Error('failed'));

Running Tasks

Execute tasks with .run():

typescript
const task = FuturableTask.of(() => fetch('/api/data'));

// Simple run
const result1 = await task.run();

// Run with external signal
const controller = new AbortController();
const result2 = await task.run(controller.signal);

// Run safely (returns Result type)
const result3 = await task.runSafe();
if (result3.success) {
  console.log(result3.data);
} else {
  console.error(result3.error);
}

Transforming Tasks

Transform results with functional methods:

typescript
const task = FuturableTask
  .of(() => fetch('/api/users'))
  .map(res => res.json())                    // Transform
  .map(users => users.filter(u => u.active)) // Chain transformations
  .flatMap(users =>                          // Flatten nested tasks
    FuturableTask.of(() => enrichUsers(users))
  );

Comparison Examples

Promise vs Futurable vs FuturableTask

typescript
// ❌ Promise: Executes immediately
const promise = fetch('/api/data')
  .then(res => res.json());
// Fetch already started!

// ✅ Futurable: Executes immediately but cancellable
const futurable = Futurable.fetch('/api/data')
  .then(res => res.json());
// Fetch already started!
futurable.cancel(); // Can cancel

// ✅ FuturableTask: Lazy, cancellable, reusable
const task = FuturableTask
  .of(() => fetch('/api/data'))
  .map(res => res.json());
// Nothing executed yet!

const result1 = await task.run(); // Now it executes
const result2 = await task.run(); // Executes again independently
task.cancel(); // Cancel all future runs

Benefits

1. No Premature Execution

typescript
// With Promise - executes immediately
function createPromise() {
  console.log('Executing!');
  return Promise.resolve(42);
}
const p = createPromise(); // Logs "Executing!" right away

// With FuturableTask - lazy
function createTask() {
  return FuturableTask.of(() => {
    console.log('Executing!');
    return 42;
  });
}
const t = createTask(); // Nothing logged
await t.run(); // Logs "Executing!" only now

2. Reusability

typescript
// Promise - single use
const promise = fetch('/api/data');
await promise; // First use
await promise; // Same result (cached)

// FuturableTask - multiple independent executions
const task = FuturableTask.of(() => fetch('/api/data'));
await task.run(); // First execution
await task.run(); // Second independent execution

3. Composition Before Execution

typescript
// Build complex pipelines without executing
const basePipeline = FuturableTask
  .of(() => fetch('/api/data'))
  .map(res => res.json());

// Extend the pipeline
const pipelineWithRetry = basePipeline.retry(3);
const pipelineWithTimeout = basePipeline.timeout(5000);
const pipelineWithBoth = basePipeline.retry(3).timeout(5000);

// Execute only the one you need
const result = await pipelineWithBoth.run();

4. Better Testing

typescript
// Easy to test without execution
describe('Task Pipeline', () => {
  it('should compose correctly', () => {
    const task = FuturableTask
      .of(() => fetch('/api/data'))
      .map(res => res.json())
      .map(data => data.users);

    // Can inspect the task structure without running it
    expect(task).toBeDefined();
  });

  it('should execute correctly', async () => {
    // Mock the fetch
    global.fetch = vi.fn(() =>
      Promise.resolve(new Response(JSON.stringify({ users: [] })))
    );

    const task = FuturableTask
      .of(() => fetch('/api/data'))
      .map(res => res.json())
      .map(data => data.users);

    const result = await task.run();
    expect(result).toEqual([]);
  });
});

Next Steps

Explore the different capabilities of FuturableTask:

Released under the MIT License.