From a35f471d1cb5841c3d4e721f6bae48f7c0ff60c9 Mon Sep 17 00:00:00 2001 From: bisubus Date: Sat, 25 Jul 2026 19:58:35 +0300 Subject: [PATCH 1/5] (docs) Update readme files --- README.md | 540 +++++++------------- examples/README.md | 135 ++++- packages/canc-coroutine/README.md | 434 +++++++++------- packages/canc-decorators/README.md | 384 +++++++------- packages/canc-fetch/README.md | 173 ++++++- packages/canc-lazy-promise-native/README.md | 83 +++ packages/canc-lazy-promise/README.md | 135 +++-- packages/canc-promise/README.md | 343 +++++++++++-- packages/canc-toolbox-native/README.md | 85 +++ packages/canc-toolbox/README.md | 220 ++++++++ 10 files changed, 1697 insertions(+), 835 deletions(-) create mode 100644 packages/canc-lazy-promise-native/README.md create mode 100644 packages/canc-toolbox-native/README.md create mode 100644 packages/canc-toolbox/README.md diff --git a/README.md b/README.md index 4021b96..feb09e8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

- canc ⮿ a crafty foundation for cancelable promises + canc ⮿ a crafty foundation for cancelable promises

- - License - - PRs Welcome + + License + + PRs Welcome

@@ -15,404 +15,236 @@ Cancelable promise ecosystem based on native Promise: coroutines, a --- - -## Introduction ---> -## 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 +```ts +import * as canc from '@cancjs/coroutine'; +import { cancelableFetch } from '@cancjs/fetch'; -### Cancelable Promises - -Cancellation-aware promise utilities: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameVersionDescription
- @cancjs/promise - - - Version - - - Cancelable promise implementation based on ES Promise -
- @cancjs/coroutine - - - Version - - - Cancelable generator-based drop-in replacements for async..await and async iterators -
- @cancjs/fetch - - - Version - - - Cross-platform Fetch API that uses cancelable promises -
- @cancjs/lazy-promise - - - Version - - - Cancelable lazily evaluated promise-like class -
- @cancjs/toolbox - - - Version - - - 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: - - - - - - - - - - - - - - - - - - - - - - - - - - -
PackageVersionDescription
- @cancjs/coroutine-native - - - Version - - - Generator-based drop-in replacements for async..await and async iterators -
- @cancjs/lazy-promise-native - - - Version - - - Lazily evaluated promise-like class -
- @cancjs/toolbox - - - Version - - - A collection of promise helper functions -
- - +const loadTrip = canc.async(function* (tripId: string) { + const response = yield* canc.await(cancelableFetch(`/api/trips/${tripId}`)); + const trip = yield* canc.await(response.json()); -## How It Works + const [hotels, flights] = yield* canc.await.all([ + searchHotels(trip.city), + searchFlights(trip.city), + ]); -Cancellation is a special form of promise rejection with cancel error that triggers registered handlers for the entire cancellation-aware promise chain. + return { trip, hotels, flights }; +}); -`canc` promises implement two-way cancellation mechanism that treats promise chains as subscriptions: +const pending = 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: requests in flight +// are aborted, the steps after them never run. +pending.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 built on native `Promise`, cancellation is a rejection you can catch +* 🔄 two-way cancellation: it flows down the chain and bubbles back up when nobody wants the value +* ⚡ generator coroutines that replace `async`/`await` and `async function*` +* 🌐 cancelable Fetch API, with `AbortSignal` interop in both directions +* 🧰 utility toolbox (`delay`, `timeout`, `retry`, `waitFor`) plus adapters that make existing APIs cancelable +* 💤 lazily evaluated promises that never start if nobody subscribes +* 🎀 decorators for TypeScript and Babel +* 🔌 third-party integrations for axios, RxJS, React, Express and more +* 🧬 twin packages on plain `Promise` for the parts that do not need cancellation +* 📦 CJS, ESM and UMD builds for modern and legacy browsers and Node.js +* 🔷 TypeScript-first, with types inferred through every step -## Motivation +## Packages -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. +### Core - +| Package | Native twin | Description | +|---|---|---| +| [`@cancjs/promise`](packages/canc-promise) | ⚪ | Cancelable promise implementation based on ES `Promise` | +| `@cancjs/promise-legacy` 🚧 | ⚪ | The same core for older engines and polyfilled `Promise` | +| [`@cancjs/coroutine`](packages/canc-coroutine) | ⚪ | Cancelable generator-based drop-in replacements for `async`/`await` and async iterators | +| [`@cancjs/decorators`](packages/canc-decorators) | ⚪ | Class-method decorators for coroutines, in all three decorator dialects | -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. +### Extended -See [examples](#examples) for more use cases. +| Package | Native twin | Description | +|---|---|---| +| [`@cancjs/fetch`](packages/canc-fetch) | ⚪ | Cross-platform Fetch API that returns cancelable promises | +| [`@cancjs/lazy-promise`](packages/canc-lazy-promise) | [`@cancjs/lazy-promise-native`](packages/canc-lazy-promise-native) | Lazily evaluated promise-like, the executor runs on first subscription | +| [`@cancjs/toolbox`](packages/canc-toolbox) | [`@cancjs/toolbox-native`](packages/canc-toolbox-native) | Helper functions, ponyfills and adapters for cancellation-aware code | - +Cancellation only reaches as far as the chain does, so libraries that own the work need an adapter. +These wrap a third-party library so its requests, subscriptions and handlers join the same +cancelable chain. Every one of them is prototyped as a working project in +[examples](examples) before it becomes a package, so the patterns are usable today. -### Background +| Package | Description | +|---|---| +| `@cancjs/axios` 🚧 | Axios instances whose request methods return cancelable promises | +| `@cancjs/react` 🚧 | Hooks that tie a cancelable task to a component lifecycle | +| `@cancjs/express` 🚧 | Middleware that cancels a handler chain when the client disconnects | +| `@cancjs/rxjs` 🚧 | Conversion between cancelable promises and observables, without losing cancellation | -* **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. +⚪ no native twin, 🚧 planned, not released yet. -* **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. +## How It Works -* **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. +Cancellation is a special form of promise rejection with cancel error that triggers registered +handlers for the entire cancellation-aware promise chain. -* **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. +`canc` promises implement two-way cancellation mechanism that treats promise chains as +subscriptions: -### Comparison +* cancellation propagates down the promise chain when parent promise is canceled -| | `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) | +* cancellation bubbles up the chain when all child promises are canceled and parent promise value + is no longer consumed -`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. +Bubbling can be turned off per promise when the work causes side effects that should not be +discarded implicitly. Both directions work through `all`, `race` and the other combinators, and +through every coroutine step. -## Performance +A chain is cancelable only if it consists of `canc` promises. This requires cancellation-aware +wrappers for Fetch API and third-party libraries, and `async`/`async*` functions need to be +replaced with generator-based coroutines. The full model lives in +[`@cancjs/promise`](packages/canc-promise#how-it-works). -`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. - - +## Motivation -## Compatibility +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. -Packages rely on following ECMAScript 2015+ features: `Symbol` (ES2018 for async iterators), `Reflect`, `Promise` (ES2018 for `finally`, ES2020 for `allSettled`), `Object.assign`, `Object.setPrototypeOf`. +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. -### TypeScript +### The problem, in code -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`. +Plain `async`/`await` cannot be interrupted. The caller walks away, the work does not: -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 +async function loadTrip(tripId: string) { + const trip = await fetchTrip(tripId); + // The user already left. Both requests below still go out. + const hotels = await searchHotels(trip.city); + const flights = await searchFlights(trip.city); + return { trip, hotels, flights }; +} +``` -### Build targets +`AbortController` fixes it, 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, so the gaps need manual guards. + const hotels = await searchHotels(trip.city); + signal.throwIfAborted(); + const flights = await searchFlights(trip.city, signal); + return { trip, hotels, flights }; +} -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` -(`