@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?
- Installation
- Quick Start
- Core Concepts
- API Reference
- Examples
- Runtime Support
- Contributing
- License
π― 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);
}
Real-World: Debounced Search #
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 #
- Tree-shakeable: All exports are functions, no classes in public API
- Web Standards: Built on native async iterators and promises
- Zero Dependencies: Only development dependencies
- Type-Safe: Full TypeScript support with strict types
- 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:
π Related Projects #
- @logtape/logtape - Logging library
- @std/async - Deno async utilities
Documentation β’ GitHub β’ Issues
Made with β€οΈ by Okiki Ojo