Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
482 changes: 125 additions & 357 deletions README.md

Large diffs are not rendered by default.

Binary file removed assets/canc-logo.png
Binary file not shown.
1 change: 1 addition & 0 deletions assets/canc-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 119 additions & 15 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,134 @@
# Examples

Runnable examples import `@cancjs/*` through the workspace symlinks in `node_modules` (set up by
`npm install` at the monorepo root), so run them from the repo root after `npm install` / `npm run build`.
Every example is one small application written twice: `-vanilla` and `-canc`. Same files, same
function names, same order, so the two versions are meant to be read as a diff. The vanilla side
is not a strawman. Where cancellation is the point of the example, it carries the real
`AbortController` attempt with its guards and cleanup, and the comparison is against that.

`@shared/*` packages (under `_shared/`) are examples-internal workspace packages, never
published — the scope reads like a path alias but resolves through the normal workspace symlink.
The `app-*` projects integrate with a framework, a server or a library. The `demo-*` projects are
smaller and each teaches one part of the ecosystem.

## demo-promise-basics
## Running

Vanilla-Promise vs `@cancjs/promise` twin scripts showing the same fetch chain, one canceled
partway through with a plain `AbortController` flag check, the other with `cancel()`.
The examples consume the built `dist` of each package through npm `file:` dependencies, and this
directory is a separate npm project, so it installs on its own.

```sh
# in the monorepo root
npm install
npm run build

# here
cd examples
npm install
```

Then run either flavor of an example:

```sh
cd demo-promise-basics
npm run start:vanilla
npm run start:canc
```

Frontend examples add `dev:vanilla` and `dev:canc` for a dev server with hot reload, and
`build:vanilla` / `build:canc` for a production build.

Everything at once, from the monorepo root:

```sh
npm run examples:test
npm run examples:typecheck
```

See `demo-promise-basics/README.md` for the runnable start scripts.
Each example has its own README with the file map, what to diff, and an honesty note about which
layer cancellation actually reaches.

## React and state management

* [app-react](app-react) travel search typeahead. Every keystroke cancels the previous search, so
a slow response cannot overwrite a newer one. Hover prefetch cancels on unhover.
* [app-react-suspense](app-react-suspense) destination details under Suspense. A cancelable
boundary aborts the load the user walked away from.
* [app-react-zustand](app-react-zustand) album and track browser. Switching albums fast cancels
the store action still in flight.

## Vue

* [app-vue](app-vue) marketplace catalog. A cancelable watch cancels the previous run before
starting the next, which is the awaited-watch footgun in plain Vue.
* [app-vue-pinia](app-vue-pinia) checkout wizard. Leaving a step cancels the calls that step
started.
* [app-vue-suspense](app-vue-suspense) product page under `<Suspense>`, cancellation on scope
teardown.

## Angular

* [app-angular](app-angular) orders admin with a detail pane. The same service is built twice, one
with the coroutine decorator and one wiring `cancAsync` by hand.

## Servers and databases

* [app-express-kysely](app-express-kysely) slow report endpoint over SQLite. Client disconnect
cancels the remaining query chain.
* [app-fastify-mongoose](app-fastify-mongoose) hotel availability search. The route handler is a
coroutine, the repository is cancelified, so no signal is threaded through the service.
* [app-nestjs-typeorm](app-nestjs-typeorm) invoicing API. An interceptor cancels the request scope,
and a bulk endpoint rolls its transaction back in a shielded `finally`.

## AI and streaming

* [app-ai-chat-stop](app-ai-chat-stop) support chat with a Stop button that stops the spend, across
browser, server and the model call.
* [app-ai-rag-pipeline](app-ai-rag-pipeline) retrieval pipeline (embed, retrieve, rerank, stream)
as one cancelable flow. No API key needed.
* [app-ws-progress](app-ws-progress) video export progress over a WebSocket. Cancel stops the
transcode, not just the progress bar.

## Third-party libraries

* [app-axios](app-axios) an axios instance whose request methods return cancelable promises,
against the manual request registry it replaces.
* [app-rxjs](app-rxjs) log viewer where an observable stream drives promise-based work.
`switchMap` alone does not stop it, and this is what does.

## CLI and concurrency

* [app-cli-graceful](app-cli-graceful) site backup CLI. Ctrl-C cancels the whole task tree
gracefully, a second one exits immediately.
* [app-crawler-race](app-crawler-race) site health crawl through a concurrency pool. One cancel
prunes the entire in-flight subtree.

## Concept demos

* [demo-promise-basics](demo-promise-basics) cancellation as a rejection, cancel handlers, and the
two-way propagation an `AbortController` cannot express.
* [demo-chain-propagation](demo-chain-propagation) propagation down and bubbling up, with
`bubble: false` and `shield: true` in context.
* [demo-combinators](demo-combinators) what `all`, `any`, `race` and `allSettled` do to the losers.
* [demo-coroutine](demo-coroutine) `cancAsync` and `cancAwait` through a checkout flow, including
an acknowledged cancellation gap.
* [demo-fetch](demo-fetch) cancelable requests, external signals, pre-aborted signals, timeout
composition.
* [demo-toolbox](demo-toolbox) pollers, retries, delays and timeouts under cancellation.
* [demo-decorators](demo-decorators) one client class wired four ways, one per decorator dialect
plus the manual form.
* [demo-signal-interop](demo-signal-interop) bridges in both directions between signals and
cancelable promises.
* [demo-async-dispose](demo-async-dispose) `await using` scopes, cleanup ordering, shield survival,
disposal after settle.

## app-react-suspense
## Shared code

Travel details loaded under React Suspense. A `CancelableSuspense` boundary cancels the abandoned
request when the user picks another destination mid-load, next to a naive in-child attempt that
leaks. See `app-react-suspense/README.md`.
Packages under `_shared/` are internal to the examples and are never published. The `@shared`
scope reads like a path alias and resolves through the normal workspace symlink.

## app-vue-suspense
* `@shared/mock-api` fake domain APIs that log their calls, including an `aborted` marker so an
example can prove a request was really stopped
* `@shared/util` small cross-example helpers such as `sleep`
* `@shared/unhandled-rejection` and its browser twin, an app-wide guard that ignores `CancelError`
and lets real rejections surface

Product detail page under Vue `<Suspense>`. A generator setup wrapped by `cancelableSetup` cancels
the in-flight load when the component's scope tears down. See `app-vue-suspense/README.md`.
Code under an example's `src/lib/` is written to be copied. It is general-purpose enough to be
extracted into a package later, so treat it as a starting point for your own hooks, composables
and adapters.
130 changes: 98 additions & 32 deletions packages/canc-axios/README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,44 @@
<p align="center">
<img src="../../assets/canc-logo.png" width="483" title="canc &#x2BBF; A crafty foundation for cancelable promises" alt="canc &#x2BBF; a crafty foundation for cancelable promises">
</p>
<div align="center">
<img src="https://raw.githubusercontent.com/cancjs/canc/master/assets/canc-logo.svg" style="width: 400px; max-width: 100%; height: auto;" title="canc &#x2BBF; A crafty foundation for cancelable promises" alt="canc &#x2BBF; A crafty foundation for cancelable promises">
<div>&nbsp;</div>
</div>

<h1 align="center">@cancjs/axios</h1>

<p align="center">
<a href="../../LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License"></a>
Axios instances whose request methods return cancelable promises.
</p>

---

## Introduction

A drop-in replacement for the axios default export whose request methods return a
CancelablePromise. Calling `.cancel()` aborts the request and rejects with a `CancelError`, so
`CancelablePromise`. Calling `.cancel()` aborts the request and rejects with a `CancelError`, so
cancellation reads as an ordinary rejection in `try`/`catch`.

The wrapper holds no state of its own. It forwards to a real axios instance, so config merging,
header handling, interceptor chains and `create()` seeding are still axios's own code.

## Features

- same call shapes and signatures as axios, including the full `AxiosResponse` result
- `.cancel()` aborts the in-flight request through an AbortSignal
- a caller-supplied `config.signal` also rejects with a `CancelError`
- interceptors receive a cancel context, and a cancelable promise they return is canceled with the
request
- instances from `create()` are wrapped too, with their own defaults and interceptors
- works with the xhr, http and fetch adapters, and with axios 0.22 and up
- no Proxy, ES5-friendly output
* same call shapes and signatures as axios, including the full `AxiosResponse` result
* `.cancel()` aborts the in-flight request through an AbortSignal
* cancellation is supported through the full request lifecycle, including interceptors
* a caller-supplied `config.signal` also rejects with a `CancelError`
* instances from `create()` are wrapped too, with their own defaults and interceptors
* works with the xhr, http and fetch adapters, and with axios 0.22 and up
* no Proxy, ES5-friendly output

## Getting Started

### Installation

#### NPM

```
npm i -S @cancjs/axios
```

#### Yarn

```
yarn add @cancjs/axios
```sh
npm install @cancjs/axios axios @cancjs/promise
```

Requires `axios` (0.22 or later) and `@cancjs/promise` alongside it.
`axios` (0.22 or later) and `@cancjs/promise` are peer dependencies.

### Usage

Expand Down Expand Up @@ -85,8 +77,47 @@ import { cancelableAxios } from '@cancjs/axios';
const api = cancelableAxios.wrap(axios.create({ baseURL: 'https://api.example.com' }));
```

Interceptors get a second argument carrying the cancellation of the request they run for. A
cancelable promise returned from an interceptor is canceled along with the request:
In a coroutine the request joins the surrounding cancellation:

```ts
import * as canc from '@cancjs/coroutine';

const loadIssues = canc.async(function* (query: string) {
const response = yield* canc.await(api.get('/issues', { params: { q: query } }));
return response.data;
});

const pending = loadIssues('bug');
pending.cancel(); // aborts the request
```

## How It Works

Each request method (`get`, `post`, `put` and the rest) creates its own `AbortController`, merges
the signal into the request config, and returns a `CancelablePromise`. Canceling the promise
aborts that controller, so axios handles the transport-level abort through whichever adapter it is
using (xhr, http or fetch).

Cancellation is supported through the full lifecycle of a request, not just the network call.
When a request is canceled, interceptors that are still running are canceled too. An interceptor
that returns a cancelable promise has that promise canceled along with the request, so work
started in an interceptor (a token refresh, a retry) does not keep going after the caller has
walked away.

An existing `config.signal` from the caller is composed with the internal one. Either source can
abort the request, and in both cases the promise rejects with a `CancelError`, normalizing the
error regardless of which signal aborted.

The wrapper holds no state beyond what axios itself holds. `create()` returns another wrapped
instance with its own defaults and interceptors, and `wrap()` wraps an existing axios instance
without creating a new one.

## Description

### Interceptors

Interceptors receive a second argument carrying the cancellation context of the request they run
for. The context exposes a signal and a check for whether the request has already been canceled:

```js
api.interceptors.request.use((config, ctx) => {
Expand All @@ -101,20 +132,55 @@ api.interceptors.request.use((config, ctx) => {
});
```

## Documentation
A cancelable promise returned from an interceptor is canceled along with the request. In the
example above, if the request is canceled while the token refresh is in flight, the refresh
promise is canceled too.

### Combinators

`all` differs from `axios.all` on purpose: it builds a `CancelablePromise`, so canceling one
request rejects the aggregate and cancels the rest. `spread` is unchanged.

### Accessing the underlying instance

The wrapped instance is reachable as `.axios` when a plain native promise is needed, for example
when handing a request to code that does not understand cancellation.

## API

`cancelableAxios` is both the default export and a named export. It mirrors the full axios
interface: `request`, `get`, `delete`, `head`, `options`, `post`, `put`, `patch`, `getUri`,
`create`, `defaults`, `interceptors`, `all`, `spread`.

`cancelableAxios.create(config?)` returns a new wrapped instance.

`cancelableAxios.wrap(axiosInstance)` wraps an existing axios instance.

`.axios` on any wrapped instance returns the underlying axios instance.

Members axios added after 0.22 (`postForm`, `AxiosHeaders`, `HttpStatusCode` and the rest) are
mirrored only when the installed axios has them.

`all` differs from `axios.all` on purpose: it builds a `CancelablePromise`, so canceling one
request rejects the aggregate. `spread` is unchanged.
## Compatibility

Axios 0.22 and later, which is when signal support was added. Node.js 18 and later, current
browsers. Everything else follows
[`@cancjs/promise`](https://github.com/cancjs/canc/tree/master/packages/canc-promise#compatibility).

## Documentation

The wrapped instance is reachable as `.axios` when a plain native promise is wanted.
* [`@cancjs/promise`](https://github.com/cancjs/canc/tree/master/packages/canc-promise) for the
cancellation model
* [Coroutines](https://github.com/cancjs/canc/tree/master/packages/canc-coroutine) for using
axios inside a cancelable flow
* [Examples](https://github.com/cancjs/canc/tree/master/examples): `app-axios` for a side-by-side
comparison against a manual request registry
* [Repository](https://github.com/cancjs/canc) for the ecosystem overview

## Contributing

You are welcome to participate through issues and pull requests!

## License

[MIT](LICENSE)
[MIT](../../LICENSE)
Loading