diff --git a/README.md b/README.md
index 4021b96..849e8a5 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
-
-
-
+
+

+
+
-
-
-
-
-
-
+
+
+[](LICENSE)
+[](#contributing)
+
+
Cancelable promise ecosystem based on native Promise: coroutines, async iterators, decorators, utilities, third-party library helpers.
@@ -15,404 +16,171 @@ Cancelable promise ecosystem based on native Promise: coroutines, a
---
-
-## Features
+
-* cancelable promise implementation built on top of ES Promise
-* generator-based cancelable replacements for `async..await` and async iterators
-* lazily evaluated cancelable promises
-* cancelable Fetch API
-* utility toolbox (`delay`, `timeout`, etc)
-* library helpers (Axios, Bluebird, RxJS, etc)
-* decorators for TypeScript and Babel
-* base packages to be used with custom promise implementation
-* UMD and ESM builds for modern and legacy browsers and Node.js
-* TypeScript-ready
-
-## Packages
-
-### Cancelable Promises
-
-Cancellation-aware promise utilities:
-
-
-
-
- | Name |
- Version |
- Description |
-
-
-
-
- |
- @cancjs/promise
- |
-
-
-
-
- |
-
- Cancelable promise implementation based on ES Promise
- |
-
-
- |
- @cancjs/coroutine
- |
-
-
-
-
- |
-
- Cancelable generator-based drop-in replacements for async..await and async iterators
- |
-
-
- |
- @cancjs/fetch
- |
-
-
-
-
- |
-
- Cross-platform Fetch API that uses cancelable promises
- |
-
-
- |
- @cancjs/lazy-promise
- |
-
-
-
-
- |
-
- Cancelable lazily evaluated promise-like class
- |
-
-
- |
- @cancjs/toolbox
- |
-
-
-
-
- |
-
- A collection of cancellation-aware promise helper functions and ponyfills
- |
-
-
-
-
-### Native Promises
-
-General-purpose promise utilities that use built-in `Promise` as promise implementation where applicable:
-
-
-
-
-
-## How It Works
+```ts
+import * as canc from '@cancjs/coroutine';
-Cancellation is a special form of promise rejection with cancel error that triggers registered handlers for the entire cancellation-aware promise chain.
+const loadTrip = canc.async(function* (tripId: string) {
+ const hotels = yield* canc.await(searchHotels(tripId));
+ const flights = yield* canc.await(searchFlights(tripId));
+ return { hotels, flights };
+});
-`canc` promises implement two-way cancellation mechanism that treats promise chains as subscriptions:
+const tripPromise = loadTrip('lis-42');
-* cancellation propagates down the promise chain when parent promise is canceled
-
-* cancellation bubbles up the chain when all child promises are canceled and parent promise value is no longer consumed
-
-Cancellation bubbling can be explicitly disabled on parent promise in case a promise causes side effects that shouldn't be implicitly discarded.
+// The user navigated away. One call stops the whole tree.
+tripPromise.cancel();
+```
-Two-way cancellation mechanism is supported for all common ways to establish a promise chain, including `all`, `race`, etc composition methods and coroutine `yield`.
+## Features
-A chain is cancelable only if it consists of `canc` promises. This requires to use cancellation-aware wrappers for Fetch API and third-party librararies, `async` and `async*` need to be replaced with cancelable generator-based coroutines.
+- ❎ cancelable promise that extends native `Promise`
+- 🔃 two-way, deep cancellation: propagates down the chain, bubbles up when a value goes unclaimed
+- ⚡ generator coroutines as drop-in replacements for `async`/`await` and async iterators
+- 🧰 utility toolbox: `debounce`, `delay`, `timeout`, lazy evaluation and other helpers
+- 🔌 cancelable `fetch` and third-party integrations: React, Express, Axios and more
+- 🎀 class method decorators: standard and legacy (TypeScript, Babel)
+- 🧬 twin ecosystem packages for native `Promise`
+- 📦 cross-platform: Node.js, Deno, Bun and browsers
+- 🟦 extensive TypeScript support
## Motivation
-Promise cancellation is highly beneficial in real life scenarios yet it's not a part of existing ECMAScript specification. JavaScript API like Fetch `AbortController` use their own mechanisms that aren't unified with native promises.
-
-
-
-A situation that is common in modern JavaScript applications is that a process like network request that stands behind long-running asynchronous task is abortable, consumers need to unsubscribe from results and abort initial process when it's no longer needed. This eventually becomes harder with uncancelable promises when a task is composed of smaller independent tasks.
-
-See [examples](#examples) for more use cases.
-
-
-
-### Background
-
-* **No official solution**. [Native cancelable promises](https://github.com/tc39/proposal-cancelable-promises) were incompatible with ES6 promise semantics, provided one-way cancellation, used unwieldy cancel tokens and have been abandoned.
-
-* **Bluebird stepped aside**. Bluebird has bulky stable API and has been largely superseded by ES promises where applicable, particularly due to `async..await`. [Two-way cancellation](http://bluebirdjs.com/docs/api/cancellation.html) is disabled by default and incompatible with native promise semantics.
+Cancellation is standard in languages with async primitives, from Kotlin coroutines to Swift tasks to Rust futures. JavaScript is an unfortunate exception: a promise has no way to be stopped once started, and `async`/`await` is syntax sugar over promises, so it inherits the same limitation.
-* **No universal third-party options**. JavaScript community provides no comprehensive alternatives based on native promises. Renowned `p-*` [package collection](https://github.com/sindresorhus/promise-fun#packages) only supports one-way cancellation and targets Node.js.
+In practice this means wasted work. A component unmounts while its data is still loading. A route changes but the previous page's API calls keep going. A user cancels a search but the requests and retries continue. The deeper a task is composed of subtasks, the harder it gets to stop cleanly.
-* **Observables aren't a magic bullet**. Observables can provide a superset of promise features, as well as cancellation. However, observables don't offer expressive sugar similar to `async..await`, cancellation may be lost in promise interop. Observables are push-based and cannot displace pull-based `async*` async iterators. [RxJS](https://github.com/ReactiveX/rxjs) is commonly used implementation with complex API, no [native observable](https://github.com/tc39/proposal-observable) implementation exists yet.
+The platform has `AbortController`. It gives you a signal you can pass to `fetch` and other APIs, but the signal has to be threaded through every function call, gaps between steps need manual guards, and `AbortError` has to be filtered from real errors at every catch boundary. For a single `fetch` call this is manageable. For a chain of requests, parallel work and third-party libraries that may or may not accept a signal, the boilerplate adds up fast and the signal micromanagement becomes a cost of its own.
-### Comparison
+`canc` moves cancellation into the promise chain itself. Cancel one promise and every step below it stops. Combinators carry the same behavior: `race` cancels the losers, and when every promise inside an `all` is canceled, the `all` itself is canceled too. No signals to thread, no guards to write.
-| | `canc` | native + `AbortController` | [p-cancelable](https://github.com/sindresorhus/p-cancelable) | [alkemics/CancelablePromise](https://github.com/alkemics/CancelablePromise) | [c-promise2](https://github.com/DigitalBrainJS/c-promise) | [Bluebird](http://bluebirdjs.com/docs/api/cancellation.html) |
-|---|---|---|---|---|---|---|
-| Native `Promise` subclass | yes | n/a | no (wraps) | no (wraps) | no (wraps) | no (own implementation) |
-| Deep (chain-wide) cancellation | yes | manual (thread signal yourself) | no (single promise) | no (single promise) | yes | yes |
-| Rejection-based (normal try/catch) | yes | yes (`AbortError`) | yes | no (silent skip) | no (never settles) | no (never settles by default) |
-| Two-way propagation (bubble up + flow down) | yes | no | no | no | no | no |
-| `AbortSignal` interop | yes (`@cancjs/fetch`, toolbox helpers) | native | no | no | no | no |
-| Actively maintained | yes | n/a (platform) | yes | no | no | no (cancellation feature frozen) |
+### The problem, in code
-`canc` is the only entry that combines a native `Promise` subclass with deep, two-way,
-rejection-based cancellation. `AbortController` is the maintained platform primitive but only
-threads a signal, it doesn't propagate cancellation through a chain on its own. The `p-*`-style
-packages and alkemics wrap a single promise and skip silently instead of rejecting. c-promise2 has
-deep cancellation but isn't a `Promise` subclass and has gone quiet. Bluebird's two-way
-cancellation predates today's `async..await`-centric ecosystem, is off by default, and promises
-never settle on cancel instead of rejecting.
+Plain `async`/`await` cannot be interrupted. The caller walks away, the work does not:
-## Performance
+```ts
+async function loadTrip(tripId: string) {
+ const trip = await fetchTrip(tripId);
+ // The user already left. Both searches still go out.
+ const [hotels, flights] = await Promise.all([
+ searchHotels(trip.city),
+ searchFlights(trip.city),
+ ]);
+ return { trip, hotels, flights };
+}
+```
-`canc` wraps every `Promise` operation in cancellation bookkeeping, and that costs something.
-Full numbers (methodology, machine specs, per-suite breakdowns, browser-lane results) live in
-[`docs/benchmarks.md`](docs/benchmarks.md); the short version:
-
-In a simulated request waterfall (5 sequential + 3 parallel requests, 30% canceled mid-flight),
-`canc` runs well under a microsecond slower per request than a hand-rolled native `Promise` +
-`AbortController` baseline, a relative tax that has ranged from roughly 20% to 80% across repeated runs depending on machine
-load, typically landing in the 25-50% band, that is
-dwarfed by any real network or timer latency. **For I/O-bound flows (the common case), this
-overhead is negligible.** A fraction of a microsecond of bookkeeping disappears next to a request
-that takes milliseconds. The cost shows up more in tight, cancellation-heavy loops (a
-mount/unmount-cancel component lifecycle simulation runs several times slower than native) and in
-raw construct/chain throughput under isolated microbenchmarks, where `canc` lands roughly one order
-of magnitude behind native `Promise` and is mixed against Bluebird depending on the shape of the
-chain. Combinator internals (`all`/`race`/`any`/`allSettled`) were reworked to skip an extra
-per-item allocation; the measured effect ranges from a modest win to roughly noise-level depending
-on the case and run (see `docs/benchmarks.md`), and combinators remain well short of native and
-mixed against Bluebird.
-
-Memory follows the same pattern. A single `canc` promise costs a few hundred bytes more than a
-native one. For high-concurrency workloads (thousands of promises in flight at once, long-lived
-subscriptions, or streaming/pagination patterns that keep many chains alive), budget roughly an
-extra 2 MB of retained heap per 1,000 in-flight promises versus native. If a workload creates and
-discards promises faster than it can await them, this is the number to watch.
-
-None of this changes the tradeoff to make in a given app: `canc` gives you real, rejection-based,
-two-way cancellation without hand-wiring `AbortController` through every call site. Where that's
-worth a few hundred bytes and a low single-digit-microsecond tax per operation, it's worth using;
-where a hot loop constructs and cancels promises far faster than any I/O it wraps, measure against
-your own budget first.
-
-
+`AbortController` fixes the interruption, and the cost is spread over every layer. The signal becomes a parameter of everything, every gap between steps needs a guard, and the caller has to sort cancellation out of real failures:
+
+```ts
+async function loadTrip(tripId: string, signal: AbortSignal) {
+ const trip = await fetchTrip(tripId, signal);
+ // Not every API takes a signal, and every gap between steps needs a guard.
+ signal.throwIfAborted();
+ const [hotels, flights] = await Promise.all([
+ searchHotels(trip.city, signal),
+ searchFlights(trip.city, signal),
+ ]);
+ return { trip, hotels, flights };
+}
-## Compatibility
+const controller = new AbortController();
-Packages rely on following ECMAScript 2015+ features: `Symbol` (ES2018 for async iterators), `Reflect`, `Promise` (ES2018 for `finally`, ES2020 for `allSettled`), `Object.assign`, `Object.setPrototypeOf`.
+loadTrip('lis-42', controller.signal).catch((err) => {
+ if (err.name === 'AbortError') return; // expected, not a bug
+ throw err;
+});
-### TypeScript
+controller.abort();
+```
-TypeScript floor is 4.2. Each package ships two `.d.ts` variants and resolves the right one
-automatically, no consumer configuration needed. TS >= 4.7 reads `exports["."].types` and gets
-`dist/types/*.d.ts`. Older TS falls back to `typesVersions` (pre-4.7 resolvers don't read
-`exports.types`) and gets `dist/types-ts4.2/*.d.ts`.
+With `canc` the plumbing goes away. The signature stays clean, the steps stay readable, and canceling the returned promise stops everything below the current step:
-The `-ts4.2` variant is produced from the standard output via `downlevel-dts`, plus a follow-up
-patch for `Awaited` which predates `downlevel-dts`'s own transform coverage. Verified against
-a pinned matrix (TS 4.2 / 4.7 / 5.0 / 5.4 / latest) by compiling fixture projects against the
-built tarballs.
+```ts
+const loadTrip = canc.async(function* (tripId: string) {
+ const trip = yield* canc.await(fetchTrip(tripId));
+ const [hotels, flights] = yield* canc.await.all([
+ searchHotels(trip.city),
+ searchFlights(trip.city),
+ ]);
+ return { trip, hotels, flights };
+});
-### Build targets
+const tripPromise = loadTrip('lis-42');
-All four build outputs (CJS, ESM, UMD, minified UMD) compile from the same ES5-targeted
-TypeScript source, only the module wrapper differs. CJS is `dist/index.cjs` (`main` field), ESM
-is `dist/index.mjs` (`module` field), UMD is `dist/index.umd.js` and `dist/index.umd.min.js`
-(`