A request-ownership runtime and Web Standards framework for TypeScript.
Structured ownership for child tasks, cancellation, and scoped resources.
Nelo keeps child tasks, cancellation, and scoped resources inside the request that owns them. Instead of letting asynchronous work escape as floating promises, Nelo gives every request an explicit lifetime boundary.
Work created for a request must be awaited, cancelled, released, or explicitly transferred.
Returning a Response does not necessarily mean that all request-related work has finished. A
handler can leave tasks running, lose cancellation when a client disconnects, or keep resources
alive longer than the request that created them.
Nelo structures that work as explicit handler and delivery ownership scopes:
Request Lifetime
├── Handler Scope: middleware, c.fork(), c.use()
└── Delivery Scope: Response.body, c.delivery.fork(), c.delivery.use()
When the scope closes, Nelo checks owned tasks, propagates cooperative cancellation, waits for settlement, and releases resources in reverse acquisition order.
Important
Nelo is currently experimental and is not published to npm yet. The package API is being developed in this repository before the first public release.
import { Nelo } from "nelo";
const app = new Nelo();
app.get("/users/:id", async (context) => {
const user = context.fork("user", (signal) => fetchUser(context.params.id!, { signal }));
const feed = context.fork("feed", (signal) => fetchFeed(context.params.id!, { signal }));
return context.json({
user: await user,
feed: await feed,
});
});
const response = await app.fetch(
new Request("https://example.test/users/42"),
);The API uses standard Request, Response, Headers, URL, ReadableStream, and AbortSignal
types. Runtime-specific behavior belongs in adapters rather than application handlers.
| Primitive | Purpose |
|---|---|
app.fetch(request) |
Runs routing, middleware, the handler, and owned body delivery. |
context.fork(name, operation) |
Starts an eager OwnedTask bound to the current request. |
context.signal |
Exposes cooperative cancellation to request-owned work. |
context.use(name, acquire, cleanup?) |
Acquires a resource and releases it once in LIFO order. |
context.delivery.use(cleanup) |
Keeps a resource alive until response-body delivery ends. |
context.delivery.fork(name, operation) |
Starts work owned by response delivery. |
LifetimeScope#createChild(name) |
Creates an explicit child ownership boundary. |
context.fork() returns an awaitable OwnedTask. A task is owned from creation and retains its
name, ancestry, settlement state, and failure for diagnostics.
const profile = context.fork("profile", (signal) => loadProfile({ signal }));
return context.json(await profile);If a task is neither observed nor explicitly transferred before its scope completes, Nelo cancels it
and reports NELO_TASK_001 instead of silently accepting a floating promise.
Nelo preserves the first typed CancellationReason and propagates one AbortSignal through child
scopes and owned tasks.
Cancellation remains cooperative: JavaScript cannot forcibly stop an arbitrary promise. Operations must observe the supplied signal and stop safely.
Resources implementing Symbol.asyncDispose or Symbol.dispose can be registered directly. Other
values require an explicit cleanup callback.
const connection = await context.use(
"database",
(signal) => database.connect({ signal }),
(resource) => resource.close(),
);Resources are released once, in reverse acquisition order, after owned work settles. Handler, task, and cleanup failures remain observable and are aggregated when necessary.
context.use() remains handler-owned and is cleaned when the handler returns. A stream producer
that needs a resource after that point must register its cleanup with context.delivery.use().
app.get("/stream", async (context) => {
const resource = await openResource();
context.delivery.use(() => resource.close());
return new Response(resource.stream());
});The Delivery Scope closes after body completion, cancellation, producer error, client disconnect, or
server shutdown. Cleanup is exactly once and LIFO. The first typed NeloAbortReason is retained.
Diagnostics expose immutable handling, delivering, and terminal snapshots.
The current portable surface includes:
- standard Fetch-style
app.fetch(Request)execution; - static routes and named path parameters;
- method matching with
404and405handling; - deterministic static-over-parameter route precedence;
- global and route middleware;
- single-use middleware
next()enforcement; - centralized and customizable error handling;
json,text,fork,use, anddeliverycontext helpers;- bounded settlement diagnostics for tasks that ignore cancellation;
- handler- and delivery-phase cleanup failure reporting.
app.use(async (_context, next) => {
const response = await next();
response.headers.set("x-powered-by", "Nelo");
return response;
});
app.get("/health", (context) => context.json({ ok: true }));Each path segment is percent-decoded once. Encoded slashes stay inside one parameter value.
Semantically duplicate parameter routes are rejected, including /users/:id followed by
/users/:name for the same method.
| Capability | Portable core | Node.js | Cloudflare | Deno | Bun |
|---|---|---|---|---|---|
| Request scopes | ✅ | — | — | ✅ | — |
| Owned tasks | ✅ | — | — | ✅ | — |
| Resource cleanup | ✅ | — | — | ✅ | — |
| Client disconnect integration | — | ✅ | Planned | Planned | Planned |
| Response delivery tracking | ✅ | ✅ | Planned | Planned | Planned |
| Graceful shutdown | — | ✅ | Planned | Planned | Planned |
| Deferred work | — | Planned | Planned | Planned | Planned |
The portable core wraps Response.body to close Delivery Scope at the observable body boundary. The
Node adapter additionally maps transport disconnect, backpressure, and shutdown into that model.
- forced cancellation of arbitrary promises;
- confirmation that a client physically received every response byte;
- durable or exactly-once background execution;
- complete Cloudflare, Deno, or Bun runtime adapters.
These boundaries are intentional. Runtime behavior will only be documented as supported after the corresponding adapter and real transport tests exist.
mod.ts
src/
├── lifetime/
│ ├── cancellation.ts
│ ├── scope.ts
│ ├── task.ts
│ └── resource-stack.ts
└── web/
├── app.ts
├── context.ts
├── middleware.ts
└── router.ts
examples/
docs/adr/
Nelo uses Deno for formatting, linting, type checking, and tests. npm and TypeScript emit the ESM package and declaration files.
git clone https://github.com/lasder-ca/Nelo.git
cd Nelo
npm install
deno task fmt
deno task lint
deno task check
deno task test
npm test
npm run build
npm run check:package
npm --cache /tmp/nelo-npm-cache pack --dry-runThe current package name is nelo. The repository import map also accepts @latteworkspace/nelo as
the preferred scoped fallback. Neither package is published yet.
- Phase 1 — complete: lifetime scopes, owned tasks, typed cancellation, resource cleanup.
- Phase 2 — complete: Fetch-style application API, router, middleware, context, error boundary.
- Phase 3 — complete: Node.js adapter, real socket disconnect tests, delivery tracking, graceful shutdown, and GitHub CI.
- Phase 4 — complete: separate Handler/Delivery scopes, delivery-owned resources and tasks, typed abort reasons, cleanup-failure aggregation, and request diagnostics.
- Later: Cloudflare, Deno, and Bun adapters; explicit deferred work; diagnostics tooling.
Nelo is available under the Apache License 2.0.