E-commerce back office. A slow orders report endpoint runs a chain of kysely queries over a seeded in-memory SQLite database. When the client disconnects, the canc version cancels the handler chain so the remaining work never runs. The vanilla version keeps computing for a socket nobody is reading.
Domain: an operator opens an orders report, then closes the tab before it finishes.
The examples consume the built dist of each @cancjs/* package through a npm file:.
Build the monorepo first, then install this workspace:
cd ../../ # monorepo root (canc)
npm run build
cd examples
npm install
npm run start:vanilla --workspace=app-express-kysely
npm run start:canc --workspace=app-express-kysely
npm run test --workspace=app-express-kysely
Each entry boots the server, starts an orders report, destroys the client socket partway through, and prints how many aggregate slices ran afterwards. The canc run freezes the query log at the disconnect point; the vanilla run finishes every slice.
cancAsyncRoute(src/lib/cancelable-route.ts, canc) wraps a generator route handler as acanc.asynccoroutine and cancels it onreq.on('close'). The handler keeps the normal(req, res, next)shape and owns the response; the wrapper only adds the cancellation wiring.buildReport(canc) is acanc.asynccoroutine: a page query, a per-customer totals query, then a slow grand-total aggregate split into slices. Each step is acanc.await, so cancellation is ambient. No signal is threaded through the handler.- The vanilla twin carries both shapes:
buildReportcannot be stopped at all, andbuildReportAbortableis the hand-rolled AbortController version that re-checkssignal.abortedat every boundary. Compare the single canc coroutine against both.
src/report-service-vanilla.tsvssrc/report-service-canc.ts: the report chain, with and without cancellation. The vanilla file adds a secondbuildReportAbortablefunction showing the manual-signal cost; the canc file needs no such second flavor.src/middleware-vanilla.ts: disconnect wiring for the abortable workaround, exposing an AbortSignal the handler threads by hand. The canc flavor needs no such middleware: cancellation is wired per-route bycancAsyncRoute.src/routes-vanilla.tsvssrc/routes-canc.ts: route handlers. Vanilla needs a second/orders/report-abortableroute for the workaround; canc has one report route, written as a generator passed tocancAsyncRoute.
- Cancellation stops the chain, not a running statement. better-sqlite3 executes every query synchronously on the calling thread. Nothing here can abort a query that is already running. What cancellation does is stop BETWEEN queries: the grand-total aggregate is split into slices, and cancelling means the remaining slices never start and the response is released. That is the honest, meaningful unit of cancellation for a synchronous database.
- Real databases can go further. A Postgres driver can issue a wire-level cancel of an
in-flight statement, killing work already running on the server. That is out of scope here (no
external database to run), but the same middleware and coroutine structure applies: you would
drive the driver's cancel from the same
req.on('close').
src/lib/cancelable-route.ts and the coroutine shape in src/report-service-canc.ts are the
reusable pieces. The src/mock/ database is scaffolding for this demo, not something to copy.