https://github.com/johnny-quesada-developer/easy-cancelable-promise
CancelablePromise is a Promise that can be canceled. It is a Promise that has a status property that can be '`pending`', '`resolved`', '`rejected`' or '`canceled`'. It has an onCancel method that allows you to register a callback that will be called when the promise is canceled. It has a cancel method that allows you to cancel the promise.
https://github.com/johnny-quesada-developer/easy-cancelable-promise
Last synced: 4 months ago
JSON representation
CancelablePromise is a Promise that can be canceled. It is a Promise that has a status property that can be '`pending`', '`resolved`', '`rejected`' or '`canceled`'. It has an onCancel method that allows you to register a callback that will be called when the promise is canceled. It has a cancel method that allows you to cancel the promise.
- Host: GitHub
- URL: https://github.com/johnny-quesada-developer/easy-cancelable-promise
- Owner: johnny-quesada-developer
- License: mit
- Created: 2024-09-27T12:23:06.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-01-21T01:37:23.000Z (4 months ago)
- Last Synced: 2026-01-21T12:39:18.363Z (4 months ago)
- Language: TypeScript
- Size: 137 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# easy-cancelable-promise 🎯

**The cancelable promise you didn't know you needed.** 🚀
_Promises that respect boundaries. Cancel what you don't need._ ✨
[](https://www.npmjs.com/package/easy-cancelable-promise)
[](https://www.npmjs.com/package/easy-cancelable-promise)
[](https://github.com/johnny-quesada-developer/easy-cancelable-promise/blob/main/LICENSE)
[**GitHub**](https://github.com/johnny-quesada-developer/easy-cancelable-promise) • [**NPM**](https://www.npmjs.com/package/easy-cancelable-promise)
---
## 🎯 The Problem
You're fetching user data, but the user navigated away. Your fetch continues anyway. Memory leak. Wasted bandwidth. Potential race conditions.
**Native promises can't be canceled. Their status can't be tracked.** Once started, they run to completion. Always.
## 💡 The Solution
```ts
import { CancelablePromise } from 'easy-cancelable-promise';
const fetchUser = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
const user = await fetch('/api/user', controller).then((res) => res.json());
resolve(user);
},
);
// User navigated? Just cancel it.
fetchUser.cancel('User navigated away');
```
**Clean. Simple.** The promise handles its own cleanup. Cancel from anywhere, anytime. 🎯
---
## 🚀 Why Developers Love This Library
### 🎓 **100% Promise Compatible**
```ts
// If you know this...
const promise = new Promise((resolve, reject) => {
// ...
});
// You know this!
const promise = new CancelablePromise((resolve, reject, { onCancel }) => {
// ...
});
```
Works with `async/await`, `.then()`, `.catch()` - everything!
---
### ⚡ **Built-in Progress Tracking**
```ts
const download = new CancelablePromise(
(resolve, reject, { reportProgress }) => {
// Report progress as you go
reportProgress(25);
reportProgress(50);
reportProgress(100);
resolve('Done!');
},
);
const result = await download.onProgress((percent) => {
console.log(`${percent}% complete`);
});
console.log('Finished:', result);
```
---
### 🎯 **Lifecycle Control**
```ts
const task = new CancelablePromise((resolve, reject, { onCancel }) => {
const resource = allocate();
onCancel(() => {
resource.cleanup();
console.log('Cleaned up!');
});
// Do work...
});
task.cancel(); // Cleanup happens automatically
```
---
### 🔍 **Status Tracking**
```ts
console.log(promise.status); // 'pending'
await promise;
console.log(promise.status); // 'resolved'
promise.cancel();
console.log(promise.status); // 'canceled'
```
Track state throughout the entire lifecycle!
---
### 🎪 **Multiple Cleanup Strategies**
```ts
const download = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
let cleanup = onCancel(() => controller.abort());
const file = await fetch(url, { signal: controller.signal });
// there is nothing to abort anymore, so we can remove the old listener
cleanup();
cleanup = onCancel(() => tempFile.delete());
await saveFile(file);
},
);
```
---
### 🔗 **Utilities Included**
```ts
import {
defer,
groupAsCancelablePromise,
CancelablePromise,
} from 'easy-cancelable-promise';
// Defer - external promise control
const deferred = defer();
button.onclick = () => deferred.resolve(userData);
// Group - batch with concurrency
const batch = groupAsCancelablePromise(
[() => fetchUser(1), () => fetchUser(2), () => fetchUser(3)],
{ maxConcurrent: 2 },
);
// Static methods - just like Promise
const all = CancelablePromise.all([promise1, promise2]);
const race = CancelablePromise.race([promise1, promise2]);
```
---
## 📦 Installation
```bash
npm install easy-cancelable-promise
```
**Zero dependencies. TypeScript ready. Works everywhere.**
---
## 🎬 Quick Start
### The Basics
```ts
import { CancelablePromise } from 'easy-cancelable-promise';
const loadUserData = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
const data = await fetch('/api/user', { signal: controller.signal });
resolve(await data.json());
},
);
// Cancel anytime
loadUserData.cancel('User navigated away');
```
Works exactly like Promise, but with superpowers. ⚡
---
### Multi-Stage Operations with Cascading Cancellation
Cancel operations at different stages based on progress:
```ts
const processData = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
// Stage 1: Fetch data from API
const fetchData = api.fetch(requestId);
onCancel(() => fetchData.cancel('Request canceled during fetch'));
const data = await fetchData;
// Stage 2: Transform and save
const saveData = api.save(data);
onCancel(() => saveData.cancel('Request canceled during save'));
const result = await saveData;
resolve(result);
},
);
// User cancels during any stage? Proper cleanup happens automatically
cancelButton.onclick = () => processData.cancel('User canceled operation');
```
**Dynamic cleanup strategies that evolve with your operation.** 🎯
---
### Progress Tracking
```ts
const uploadFile = new CancelablePromise(
(resolve, reject, { onCancel, reportProgress }) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
reportProgress((e.loaded / e.total) * 100);
};
onCancel(() => xhr.abort());
xhr.onload = () => resolve(xhr.response);
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.open('POST', '/upload');
xhr.send(fileData);
},
);
uploadFile
.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
})
.then(() => showSuccess())
.catch(() => showError());
// Or with async/await
try {
await uploadFile.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
});
showSuccess();
} catch (error) {
showError();
}
// User clicks cancel
uploadFile.cancel();
```
**Built-in progress tracking. No extra libraries needed.** 📊
---
## 🌟 Core Features Deep Dive
### 1️⃣ CancelablePromise - The Foundation
The core class that extends native Promise with cancellation and lifecycle management.
#### 🎨 The Basics
```ts
import { CancelablePromise } from 'easy-cancelable-promise';
// Simple timeout
const timeout = new CancelablePromise((resolve) => {
setTimeout(() => resolve('Done!'), 1000);
});
// With cancellation
const withCancel = new CancelablePromise((resolve, reject, { onCancel }) => {
const id = setTimeout(() => resolve('Done!'), 5000);
onCancel((reason) => {
clearTimeout(id);
console.log('Canceled because:', reason);
});
});
withCancel.cancel('User navigated away');
```
#### 🎯 Executor Utilities
The third parameter gives you superpowers:
```ts
new CancelablePromise(
(
resolve,
reject,
{ cancel, onCancel, reportProgress, status, isCanceled, isPending },
) => {
// ✅ cancel: Cancel from inside
cancel('Internal cancellation');
// ✅ onCancel: Subscribe to cancellation
const cleanup = onCancel((reason) => {
console.log('Canceled:', reason);
});
// ✅ reportProgress: Report progress
reportProgress(50); // 50% complete
// ✅ status: Get current status
console.log(status()); // 'pending'
// ✅ isCanceled: Check if canceled
if (isCanceled()) return;
// ✅ isPending: Check if still pending
if (isPending()) {
// Continue work
}
},
);
```
#### ⚡ Status Tracking
Track your promise through its entire lifecycle:
```ts
const promise = new CancelablePromise((resolve) => {
setTimeout(() => resolve('Done!'), 1000);
});
console.log(promise.status); // 'pending'
promise.then(() => {
console.log(promise.status); // 'resolved'
});
// Or if canceled
promise.cancel();
console.log(promise.status); // 'canceled'
// On error
promise.catch(() => {
console.log(promise.status); // 'rejected'
});
```
**Status types:**
- `'pending'` - In progress
- `'resolved'` - Successfully completed
- `'rejected'` - Failed with error
- `'canceled'` - Canceled by user/system
#### 🎬 Lifecycle Hooks
Subscribe to cancellation from inside or outside:
```ts
// Inside the executor
const promise = new CancelablePromise((resolve, reject, { onCancel }) => {
const socket = createSocket();
onCancel(() => {
socket.close();
console.log('Socket closed');
});
socket.on('data', (data) => resolve(data));
});
// Outside the executor
promise.onCancel((reason) => {
console.log('Canceled because:', reason);
logToAnalytics('promise_canceled', { reason });
});
```
#### 🎨 Progress Tracking
Report and track progress throughout execution:
```ts
const task = new CancelablePromise((resolve, reject, { reportProgress }) => {
const steps = 10;
for (let i = 0; i < steps; i++) {
doWork(i);
reportProgress((i / steps) * 100);
}
resolve('Complete!');
});
// Track progress
task.onProgress((percent, metadata) => {
updateProgressBar(percent);
console.log(`${percent}% complete`, metadata);
});
// Chain progress tracking
task
.onProgress((p) => console.log(`Progress: ${p}%`))
.onProgress((p) => updateUI(p))
.then((result) => console.log('Done!', result));
```
#### 🎪 Cancel from Anywhere
Cancel from inside the executor or outside:
```ts
const fetchWithTimeout = new CancelablePromise(
async (resolve, reject, { cancel, onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
// control it's own timeout
const timeoutId = setTimeout(() => {
cancel('Request timeout');
}, 5000);
try {
const response = await fetch('/api/data', { signal: controller.signal });
clearTimeout(timeoutId);
resolve(await response.json());
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
},
);
// Cancel from outside (internal timeout also cancels automatically)
cancelButton.onclick = () => {
fetchWithTimeout.cancel('User canceled');
};
// Cancellation won't cause unhandled rejection, but you can catch it:
fetchWithTimeout.catch((error) => {
console.log('Request failed or canceled:', error);
});
```
---
### 2️⃣ defer - Deferred Promises
Create promises with externalized resolve/reject control:
```ts
import { defer } from 'easy-cancelable-promise';
// Basic usage
const deferred = defer();
deferred.promise.then((result) => {
console.log('Result:', result);
});
// Resolve from anywhere
setTimeout(() => {
deferred.resolve('Hello world!');
}, 1000);
// Or reject
deferred.reject(new Error('Something went wrong'));
// Or cancel
deferred.cancel('User canceled');
```
#### 🎯 Event-Based Resolution
```ts
function waitForUserInput() {
const deferred = defer();
const button = document.getElementById('submit');
const input = document.getElementById('input') as HTMLInputElement;
button.addEventListener('click', () => {
deferred.resolve(input.value);
});
// Auto-cancel after 30 seconds
setTimeout(() => {
deferred.cancel('Timeout');
}, 30000);
return deferred.promise;
}
const userInput = await waitForUserInput();
console.log('User entered:', userInput);
```
#### 🔄 Managing Long-Running Operations
```ts
class TaskManager {
private currentTask: CancelablePromise | null = null;
async executeTask(task: Task) {
// Cancel previous task if running
this.currentTask?.cancel('New task started');
// Start new task
this.currentTask = api.performTask(task);
// Return new task promise
return this.currentTask;
}
cancel() {
this.currentTask?.cancel('User canceled');
}
}
```
---
### 3️⃣ toCancelablePromise - Universal Converter
Convert anything to a CancelablePromise:
```ts
import { toCancelablePromise } from 'easy-cancelable-promise';
// From native Promise
const native = Promise.resolve('hello');
const cancelable = toCancelablePromise(native);
cancelable.cancel(); // Now cancelable!
// From value
const fromValue = toCancelablePromise(42);
console.log(await fromValue); // 42
// Already cancelable? Returns as-is
const alreadyCancelable = new CancelablePromise((resolve) => resolve('hi'));
const same = toCancelablePromise(alreadyCancelable);
console.log(same === alreadyCancelable); // true
```
---
### 4️⃣ groupAsCancelablePromise - Concurrency Control
Group multiple promises with advanced control over execution:
```ts
import { groupAsCancelablePromise } from 'easy-cancelable-promise';
const tasks = [
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3),
() => fetchUser(4),
() => fetchUser(5),
];
// Execute with concurrency limit
const group = groupAsCancelablePromise(tasks, {
maxConcurrent: 2, // Only 2 at a time
});
group.onProgress((percent) => {
console.log(`${percent}% complete`);
});
const results = await group;
console.log('All users:', results);
// Cancel all pending tasks
group.cancel('User navigated away');
```
#### 🎛️ Configuration Options
```ts
groupAsCancelablePromise(tasks, {
// Max concurrent executions (default: 8)
maxConcurrent: 3,
// Execute in order (default: false)
executeInOrder: true,
// Called before each task
beforeEachCallback: (index) => {
console.log(`Starting task ${index}`);
},
// Called after each success
afterEachCallback: (result, index) => {
console.log(`Task ${index} completed:`, result);
},
// Called when queue is empty
onQueueEmptyCallback: () => {
console.log('All tasks complete!');
},
});
```
#### 🎯 Real-World: Batch Processing
```ts
async function processBatch(items: Item[]) {
const tasks = items.map((item) => () => api.processAndSaveItem(item)); // returns CancelablePromise
return groupAsCancelablePromise(tasks, {
maxConcurrent: 5,
beforeEachCallback: (index) => {
updateProgress(`Processing item ${index + 1}/${items.length}`);
},
afterEachCallback: (result, index) => {
logSuccess(`Item ${index + 1} processed`);
},
});
}
const batch = processBatch(items);
// Track progress
batch.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
});
// Cancel if user navigates away
window.addEventListener('beforeunload', () => {
batch.cancel('Page unloading');
});
const results = await batch;
```
#### 🔄 Sequential Execution
```ts
const tasks = [() => step1(), () => step2(), () => step3()];
const sequential = groupAsCancelablePromise(tasks, {
maxConcurrent: 1,
executeInOrder: true,
});
// Guaranteed to execute in order, one at a time
const results = await sequential;
```
---
### 5️⃣ Type Guards
Runtime type checking utilities:
```ts
import { isPromise, isCancelablePromise } from 'easy-cancelable-promise';
// Check if value is a Promise
if (isPromise(value)) {
await value;
}
// Check if it's a CancelablePromise
if (isCancelablePromise(value)) {
value.cancel();
console.log(value.status);
}
```
#### 🎯 Real-World: Polymorphic Handling
```ts
function handleAsyncValue(value: unknown) {
// Already cancelable? Use full API
if (isCancelablePromise(value)) {
value.onProgress((p) => console.log(`${p}%`));
return value;
}
// Promise or value - convert to cancelable
return toCancelablePromise(value);
}
```
---
## 🎓 Comparison with Other Solutions
| Feature | easy-cancelable-promise | Native Promise | bluebird | p-cancelable |
| ----------------------------- | ----------------------- | -------------- | -------- | ------------ |
| ✅ 100% Promise compatible | ✅ | ✅ | ✅ | ✅ |
| ✅ Cancelation | ✅ | ❌ | ✅ | ✅ |
| ✅ Progress tracking | ✅ | ❌ | ❌ | ❌ |
| ✅ Status property | ✅ | ❌ | ❌ | ❌ |
| ✅ Multiple cancel listeners | ✅ | ❌ | ❌ | ❌ |
| ✅ Dynamic cleanup strategies | ✅ | ❌ | ❌ | ❌ |
| ✅ Concurrency control | ✅ | ❌ | ✅ | ❌ |
| ✅ TypeScript first | ✅ | ✅ | ⚠️ | ✅ |
| ✅ Zero dependencies | ✅ | ✅ | ❌ | ✅ |
| ✅ Bundle size | ~6KB | 0 | ~632KB | ~13KB |
---
## 🚀 Get Started Now
```bash
npm install easy-cancelable-promise
```
Then in your app:
```ts
import { CancelablePromise } from 'easy-cancelable-promise';
const task = new CancelablePromise((resolve, reject, { onCancel }) => {
const timeoutId = setTimeout(() => resolve('Done!'), 5000);
onCancel(() => clearTimeout(timeoutId));
});
task.cancel(); // That's it!
```
**Your promises, your control.** 🎉
---
## 🌐 Related Projects
- [easy-web-worker](https://www.npmjs.com/package/easy-web-worker) - Easy Web Workers with CancelablePromise support
---
## 📝 Contributing
We welcome contributions! If you have an idea for a new feature or improvement, please open an issue or submit a pull request.
---
## 📄 License
MIT License - see [LICENSE](LICENSE) for details.
---
### Built with ❤️ for developers who value control
**[⭐ Star on GitHub](https://github.com/johnny-quesada-developer/easy-cancelable-promise)** • **[📝 Report Issues](https://github.com/johnny-quesada-developer/easy-cancelable-promise/issues)** • **[📦 NPM Package](https://www.npmjs.com/package/easy-cancelable-promise)**