[READ-ONLY] Mirror of https://github.com/okikio/future.
TypeScript 100%
Dockerfile <1%
<1%

README.md

@okikio/future #

Controllable handles to asynchronous work

@okikio/future provides Future - a representation of asynchronous work that you can control, observe, and compose. Use them like Promises, but get pause, cancel, progress tracking, and more.

What is a Future? #

A Future represents work that will complete at some point in time. Unlike a Promise which fires and you can only wait for it, a Future gives you a handle to control the work itself.

// Promise: fire and forget
const promise = fetch('/api/data');
// You can only wait. No control.

// Future: controllable work
const future = from(fetch('/api/data'));
future.cancel();  // Stop the work
future.pause();   // Pause the work
// etc.

Think of a Future as a "work ticket" - you can check its status, cancel it, pause it, or wait for its result.

πŸ“– Table of Contents #

🎯 Why Future? #

The Problem with Promises #

Promises represent eventual values, but give you no control over the work producing them:

const promise = longRunningTask();
// Started! But now what?
// - Can't check progress
// - Can't pause it
// - Can't cancel it
// - Can't restart it

Futures: Controllable Async Work #

Futures represent the same thing - eventual values - but give you control:

const future = from(longRunningTask());

// Control the work
future.pause();           // Pause execution
future.resume();          // Resume execution  
future.cancel();          // Stop execution
future.reset();           // Restart from beginning

// Observe the work
future.getStatus();       // Check current status
for await (const progress of future) {
  console.log(progress);  // Track progress
}

// Use like Promise
const result = await future;  // Just like: await promise

What You Can Do #

Capability Description Example
Cancel Stop work mid-execution future.cancel()
Pause/Resume Control execution flow future.pause() then future.resume()
Progress Observe work as it happens for await (const x of future)
Restart Run the same work again future.reset() then await future
Status Check current state future.getStatus()
Compose Combine multiple futures all([f1, f2, f3])

How It Works #

Futures are built on async generators, which provide the control mechanisms. But you don't need to think about generators - just think of Futures as "controllable async work."

// Simple: use like a Promise
const data = await from(fetch('/api'));

// Advanced: leverage the control
const future = from(processLargeDataset());
future.pause();  // Pause during heavy work
setTimeout(() => future.resume(), 1000);

πŸ“¦ Installation #

# Deno
import * as Future from "jsr:@okikio/future";

# npm/Node.js
npm install @okikio/future

# Bun
bun add @okikio/future

# pnpm
pnpm add @okikio/future

πŸš€ Quick Start #

Use Like a Promise #

import { from } from "@okikio/future";

// Works exactly like Promise
const future = from(fetch('/api/data').then(r => r.json()));
const data = await future;  // That's it!

Add Control #

// Create controllable work
const future = from(async function* () {
  for (let i = 0; i < 100; i++) {
    yield i;  // Progress updates
  }
  return "done";
});

// Control execution
future.pause();                         // Pause work
setTimeout(() => future.resume(), 1000); // Resume later
setTimeout(() => future.cancel(), 5000); // Or cancel entirely

Track Progress #

const future = from(async function* () {
  yield "Fetching users...";
  const users = await fetch('/api/users').then(r => r.json());
  
  yield "Fetching posts...";
  const posts = await fetch('/api/posts').then(r => r.json());
  
  return { users, posts };
});

// See what's happening
for await (const status of future) {
  console.log(status);  // "Fetching users...", "Fetching posts..."
}

// Or just get final result
const data = await future.toPromise();

🧠 Core Concepts #

What is a Future? #

A Future is a handle to asynchronous work. It represents work that will complete at some point, and gives you control over that work.

const future = from(someAsyncWork());

// It's a handle - you can control the work
future.pause();
future.cancel();
future.getStatus();

// It's eventual - you can wait for the result
const result = await future;

Mental Model #

Think of a Future like a work order or job ticket:

  • You submit work to be done
  • You get a ticket (the Future)
  • You can check the ticket's status
  • You can cancel the work
  • You can collect the result when done
// Submit work, get ticket
const ticket = from(processData());

// Check status
ticket.getStatus();  // "running"

// Cancel if needed
if (userWantsToCancel) {
  ticket.cancel();
}

// Collect result
const result = await ticket;

Promise Compatibility #

Futures implement PromiseLike, so they work everywhere Promises do:

// All of these work
await future;
future.then(result => console.log(result));
future.catch(error => console.error(error));
Promise.all([future1, future2, future3]);

Progress and Observation #

Unlike Promises (single eventual value), Futures can report progress as work happens:

const future = from(async function* () {
  yield "Step 1 of 3";
  await doStep1();
  
  yield "Step 2 of 3";
  await doStep2();
  
  yield "Step 3 of 3";
  await doStep3();
  
  return "Complete";
});

// Observe progress
for await (const status of future) {
  updateProgressBar(status);
}

Two Ways to Consume #

Promise-style: Just get the final result

const result = await future.toPromise();

Iterator-style: Observe all intermediate values

for await (const value of future) {
  console.log(value);
}

How Futures Enable This #

Futures are implemented using async generators, which provide:

  • Pausability (generators can pause between yields)
  • Cancellation (generators have a .return() method)
  • Multiple values (generators yield sequences)
  • State preservation (generator locals persist across pauses)

But you don't need to think about generators. Just think: "Future = controllable async work."

Status Lifecycle #

Every Future goes through states:

   Idle
    ↓
  Running ←→ Paused
    ↓
  Completed
    ↓
  Destroyed

Or can be cancelled at any point:

  Idle/Running/Paused
         ↓
     Cancelled
         ↓
     Destroyed

Resource Management #

Futures use Explicit Resource Management (TC39 proposal):

// Automatic cleanup with 'using'
{
  await using future = Future.from(async function* (_, stack) {
    const file = await Deno.open("data.txt");
    stack.use(file); // Auto-cleanup
    
    const content = await file.readAll();
    yield content;
  });
  
  await future.toPromise();
} // Automatically disposed and cleaned up here

πŸ“š API Reference #

Factory Functions #

from(operation) #

Converts various types into a Future:

// From Promise
Future.from(Promise.resolve(42));

// From async generator
Future.from(async function* () {
  yield 1;
  return 2;
});

// From iterable
Future.from([1, 2, 3]);

// From ReadableStream
Future.from(response.body);

of(value) #

Creates a Future from a single value:

const future = Future.of(42);
await future.toPromise(); // 42

Concurrency Control #

all(futures) #

Run all futures concurrently (like Promise.all):

const futures = [
  Future.from(fetch('/api/user')),
  Future.from(fetch('/api/posts')),
  Future.from(fetch('/api/comments'))
];

const results = await Future.all(futures).toPromise();
// All results in order

allSettled(futures) #

Run all futures, get settled results:

const results = await Future.allSettled(futures).toPromise();

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
});

race(futures) #

Return first to complete:

const fastest = await Future.race([
  Future.from(fetchFromCDN()),
  Future.from(fetchFromBackup())
]).toPromise();

some(futures, count) #

Get first N results:

const firstThree = await Future.some(futures, 3).toPromise();

withConcurrencyLimit(futures, limit) #

Control max concurrent operations:

const futures = urls.map(url => Future.from(fetch(url)));

// Only 5 concurrent requests at a time
const results = await Future.withConcurrencyLimit(futures, 5).toPromise();

Sequential Execution #

scope(futures) #

Run futures sequentially in order:

const results = await Future.scope([
  future1,
  future2,
  future3
]).toPromise();
// Executes one after another

Background Execution #

inBackground(future) #

Execute during idle time (uses requestIdleCallback):

const bgFuture = Future.inBackground(
  Future.from(async function* () {
    // Heavy computation during idle time
    yield processData();
  })
);

const result = await bgFuture.toPromise();

Splitting and Filtering #

split(future) #

Split into success and error streams:

const [resolved, errors] = Future.split(future);

for await (const value of resolved) {
  console.log('Success:', value);
}

for await (const error of errors) {
  console.error('Error:', error);
}

splitBy(future, predicate) #

Split based on a condition:

const numbers = Future.from([1, 2, 3, 4, 5, 6]);
const isEven = (n: number) => n % 2 === 0;

const [evens, odds] = Future.splitBy(numbers, isEven);

for await (const n of evens) {
  console.log('Even:', n); // 2, 4, 6
}

for await (const n of odds) {
  console.log('Odd:', n); // 1, 3, 5
}

Manual Control #

withResolvers() #

Manually control resolution (like Promise.withResolvers):

const { future, resolve, reject } = Future.withResolvers<number>();

// Resolve later
setTimeout(() => resolve(42), 1000);

const result = await future.toPromise(); // 42

withAbortable(future, abort) #

Link to external abort signal:

const controller = new AbortController();

const future = Future.withAbortable(
  Future.from(longRunningTask()),
  controller
);

// Cancel from outside
controller.abort();

Instance Methods #

.toPromise() #

Convert to a standard Promise:

const result = await future.toPromise();

.pause() / .resume() #

Control execution flow:

future.pause();
// ... later
future.resume();

.cancel(reason?) #

Cancel the future:

await future.cancel(new Error("User cancelled"));

.reset() #

Reset for reuse (after completion):

await future.toPromise();
future.reset();
await future.toPromise(); // Run again

.clone() #

Create independent copy:

const clone = future.clone();
// Execute independently

.dispose() #

Manual cleanup:

await future.dispose();

πŸ’‘ Examples #

Real-World: API Pagination #

async function* fetchAllPages(url: string, abort: AbortController) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore && !abort.signal.aborted) {
    const response = await fetch(`${url}?page=${page}`, {
      signal: abort.signal
    });
    
    const data = await response.json();
    
    yield data.items;
    
    hasMore = data.hasMore;
    page++;
    
    // Respect rate limits
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

const future = Future.from(fetchAllPages('/api/users'));

// Collect all pages
const allItems = [];
for await (const items of future) {
  allItems.push(...items);
  
  // Can cancel if we have enough
  if (allItems.length >= 100) {
    await future.cancel();
    break;
  }
}

Real-World: Batch Processing #

async function processBatch<T>(
  items: T[],
  processor: (item: T) => Promise<void>,
  concurrency: number = 3
) {
  const futures = items.map(item =>
    Future.from(async function* () {
      yield `Processing ${item}...`;
      await processor(item);
      return `Completed ${item}`;
    })
  );
  
  return Future.withConcurrencyLimit(futures, concurrency);
}

// Process 1000 items, max 5 at a time
const batchFuture = processBatch(
  Array.from({ length: 1000 }, (_, i) => i),
  async (item) => {
    await fetch(`/api/process/${item}`, { method: 'POST' });
  },
  5
);

// Monitor progress
for await (const status of batchFuture) {
  console.log(status);
}
function createDebouncedSearch(delay: number) {
  let currentSearch: Future<any, any> | null = null;
  
  return async function search(query: string) {
    // Cancel previous search
    if (currentSearch) {
      await currentSearch.cancel();
    }
    
    currentSearch = Future.from(async function* () {
      yield `Searching for "${query}"...`;
      
      // Debounce delay
      await new Promise(resolve => setTimeout(resolve, delay));
      
      const results = await fetch(`/api/search?q=${query}`);
      return await results.json();
    });
    
    return currentSearch.toPromise();
  };
}

const search = createDebouncedSearch(300);

// Rapid calls - only last one executes
search('appl');
search('apple');
search('apple watch'); // Only this runs after 300ms

Real-World: Progressive Image Loading #

async function* loadImageProgressive(url: string, abort: AbortController) {
  // Load thumbnail first
  const thumbUrl = url.replace('.jpg', '-thumb.jpg');
  const thumbResponse = await fetch(thumbUrl, { signal: abort.signal });
  const thumbBlob = await thumbResponse.blob();
  yield URL.createObjectURL(thumbBlob);
  
  // Then load full image
  const fullResponse = await fetch(url, { signal: abort.signal });
  const fullBlob = await fullResponse.blob();
  return URL.createObjectURL(fullBlob);
}

const imageFuture = Future.from(loadImageProgressive('/images/photo.jpg'));

// Show thumbnail immediately
for await (const imageUrl of imageFuture) {
  imageElement.src = imageUrl;
}

// Final result is full image
const finalUrl = await imageFuture.toPromise();
imageElement.src = finalUrl;

🌐 Runtime Support #

@okikio/future works across all modern JavaScript runtimes:

Runtime Support Version
Deno βœ… Full 2.x+
Node.js βœ… Full 22.x+
Bun βœ… Full 1.x+
Browsers βœ… Full Modern (ES2022+)
Cloudflare Workers βœ… Full -

Required Features #

  • ES2022+ (async generators, for-await-of)
  • Explicit Resource Management (TC39 stage 3)
  • Promise.withResolvers (polyfilled if needed)

πŸ— Architecture #

Design Principles #

  1. Tree-shakeable: All exports are functions, no classes in public API
  2. Web Standards: Built on native async iterators and promises
  3. Zero Dependencies: Only development dependencies
  4. Type-Safe: Full TypeScript support with strict types
  5. Memory-Safe: Automatic cleanup via disposal patterns

Internal Structure #

Future (class)
  β”œβ”€β”€ Status management
  β”œβ”€β”€ Event dispatching
  β”œβ”€β”€ Generator wrapper
  └── Disposal stack

Factory functions (tree-shakeable)
  β”œβ”€β”€ from() - Conversion
  β”œβ”€β”€ all(), race(), etc. - Concurrency
  β”œβ”€β”€ scope() - Sequential
  β”œβ”€β”€ split(), splitBy() - Filtering
  └── inBackground() - Scheduling

🀝 Contributing #

We welcome contributions! Please see our Contributing Guide.

Development Setup #

# Clone the repository
git clone https://github.com/okikio/future.git
cd future

# Install Deno 2.x+
curl -fsSL https://deno.land/install.sh | sh

# Run tests
deno task test

# Run specific tests
deno task dev

Testing #

All features must have comprehensive tests:

# Run all tests
deno test -RW --clean --trace-leaks

# Run with coverage
deno test --coverage=./coverage

# Generate coverage report
deno coverage ./coverage

πŸ“„ License #

MIT Β© Okiki Ojo


πŸ™ Acknowledgments #

Inspired by:


Documentation β€’ GitHub β€’ Issues

Made with ❀️ by Okiki Ojo