npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@btravstack/core

v0.36.0

Published

The application kernel: boot a di module into a running process, and stop it without losing work

Readme

@btravstack/core

The application kernel: boot a @btravstack/di module into a running process, and stop it again without losing work — one lifecycle state machine, one unit-of-work registry, one Runtime contract. It knows nothing about HTTP, AMQP or Temporal, never throws, and never calls process.exit.

📖 Documentation · Get Started · Reference · API Reference

pnpm add @btravstack/core @btravstack/config @btravstack/di unthrown

@btravstack/config, @btravstack/di and unthrown are peer dependencies — install all four. The kernel itself depends on node: builtins only. Node >=22.

A worked example

An application is a di module. A runtime is a service of that module, provided on a port declared over RuntimePort; start finds it there, builds the graph, and drives what it finds. The transport starters (@btravstack/http-server, @btravstack/temporal-worker, @btravstack/amqp-worker) provide real runtimes; this one is a timer, so the sample stays self-contained:

import { Module, Port, Provider } from "@btravstack/di";
import {
  RuntimePort,
  runMain,
  start,
  type Runtime,
  type Serving,
} from "@btravstack/core";
import { Ok, OkAsync } from "unthrown";

class Greeter extends Port("Greeter")<{
  readonly greet: (name: string) => string;
}> {}

const AppModule = Module("App")({
  provides: [
    Provider(Greeter)({
      inject: {},
      value: { greet: (name: string) => `hello, ${name}` },
    }),
  ],
  exports: [Greeter],
});

// A runtime owns the transport; the kernel owns the lifecycle. This one is a
// timer, so the sample stays self-contained — no published runtime models a
// timer, and `@btravstack/http-server` would pull in a real dependency this
// sample doesn't need.
const ticker: Runtime<typeof Greeter> = {
  name: "ticker",
  resolves: [Greeter],
  start: (host) => {
    const timer = setInterval(() => {
      // Every piece of work goes through `host.run`: that is what makes it
      // count towards the drain, and what gives it an `AbortSignal`.
      //
      // The unit's `Result` is the runtime's to map — the kernel hands it back
      // and stays out of it. A timer has nowhere to return one, so it observes
      // it instead; dropping it would hide the work's `Err` *and* a `Defect`.
      void host
        .run({ kind: "tick", id: `${Date.now()}` }, (unit, signal) =>
          signal.aborted ? Ok("") : Ok(unit.ctx.get(Greeter).greet("world")),
        )
        .tapFailure((failure) => {
          process.stderr.write(`${JSON.stringify({ tick: failure.tag })}\n`);
        });
    }, 1_000);

    const serving: Serving = {
      // Stop accepting new work. In-flight units are the kernel's business.
      drain: () => {
        clearInterval(timer);
        return OkAsync();
      },
      stop: () => OkAsync(),
    };

    return OkAsync(serving);
  },
};

// A runtime is a service the module provides, on a port declared over
// `RuntimePort` — `start` finds it by that port in the module's exports. The
// composition root is what differs between an `api`, a `worker` and a
// `consumer` process; the application module is the same in all three.
class Ticker extends RuntimePort<Runtime<typeof Greeter>> {}

const TickerApp = Module("TickerApp")({
  imports: [AppModule],
  provides: [Provider(Ticker)({ inject: {}, value: ticker })],
  exports: [Greeter, Ticker],
});

await runMain(TickerApp);

runMain is the front door: it boots the module, awaits the application's exit and turns the outcome into a process exit code — one call, and the whole of a main.ts. Underneath it is start, which returns immediately with a RunningApp and decides nothing about the process. The runtime's declared needs are checked against the module's exports at compile time — a TickerApp that does not export Greeter is a type error at the call, and so is a module that exports no runtime port at all.

For a runtime author

Serving carries an optional third member, stopped — the channel a runtime reports its own death on, () => AsyncResult<void, never>. Without it the lifecycle only ever moves on a signal or a stop() call, so a worker whose poll loop died stays alive with /readyz answering 200: a pod in a Service's endpoints, consuming nothing. A runtime that can stop on its own account should implement it; the ticker above does not, because a setInterval has no third state to report, and that is why the field is optional rather than required.

The obligation is the half a signature cannot express: withdraw after a stop the kernel asked for. An arm that settles on the ordinary path races every clean shutdown. @btravstack/temporal-worker is the worked implementation, and the runtime reference states the contract in full.

Two helpers sit beside the Runtime contract, because every transport needs them and none should own a private copy:

  • releasedBy(signal, running) — running, but no later than the kernel's drain deadline, for a Serving.drain whose work settles on somebody else's clock.
  • traceIdOfTraceparent(header) — the trace-id field of a W3C traceparent, and nothing else of it: the parent's span id is dropped, since UnitMeta.traceId is a correlation id rather than a span context. Answers undefined for anything the specification calls invalid — a malformed header, an all-zero trace id, an all-zero parent id, the reserved version ff — so your runtime falls back to its own id instead of adopting one that means nothing. A higher version's extra fields are read rather than refused.

Application infrastructure exports

infrastructureExports(imports, provides) returns the publicly available Logger and HealthChecks ports from imported modules and root providers. InfrastructureExports<I, P> describes those ports at the type level. It follows whole-module re-exports while keeping private providers and business ports hidden. Application starters use it alongside their own runtime exports.

What you get

  • A drain that survives Kubernetes. On SIGTERM: readiness flips false, the kernel waits preDrainDelayMs (PRE_DRAIN_DELAY_MS, default 5_000) before telling the runtime to stop accepting — endpoint removal is eventually consistent — then in-flight work gets drainTimeoutMs (DRAIN_TIMEOUT_MS, default 20_000); whatever is still open is aborted and reported abandoned. The teardown that follows gets stopTimeoutMs (STOP_TIMEOUT_MS, default 5_000), so a release that never settles is reported as abandonedAt: "stop" rather than leaving the process in stopping with no exit report at all. A second signal stops every wait at once. The three sum to the pod's terminationGracePeriodSeconds default of 30 s, which is why the deployment sets them and the options only pin them.
  • Probes from the state machine. GET /livez and GET /readyz on PROBE_PORT (default 9000) and PROBE_HOST (default 0.0.0.0, so a kubelet httpGet probe reaches them), up before the graph is built.
  • Configuration from the environment, typed. The kernel provides @btravstack/config's Env port to every graph it boots; a bad value is a ConfigInvalid naming every fault at once, and exit 78.
  • Teardown on every path, with failing finalisers in ExitReport.teardownErrors, never masking the exit reason.
  • A per-unit scope no handler manages — the runtime calls unit.fork(module, seed) from inside host.run's work callback, and the kernel closes the scope when the unit settles.
  • An ambient record, not an ambient container — currentUnit() reads { unitId, traceId, tenantId, signal }; services never travel there. signal is the very AbortSignal the unit's work callback is handed, so a runtime whose work is a library's next() — a Temporal activity, an AMQP delivery — can still honour the drain deadline.
  • Observation as a set port, not a logger in every starter. A starter hands each operation to Observers — observe(observers, operation) for a span-shaped start-then-settle, observed(observers, operation, call) around one AsyncResult-returning call — and holds no Logger, Meter or Tracer of its own. Composing @btravstack/observability is what turns those operations into lines, spans and instruments; composing nothing costs an inert call.
  • Nothing throws. Every async surface is an unthrown AsyncResult; runMain sets process.exitCode — 0 clean, 1 a modeled startup error, 2 abandoned work or a failed finaliser, 70 a crash, 78 a bad environment.

Every sample above is compiled by src/docs-examples.test-d.ts. The rest — the Runtime contract, the drain, embedding without runMain — is on the documentation site. Testing what the kernel boots is @btravstack/testing's job — a bootFixture for test.extend, tapped to read services out of a running app, an in-memory runtime and a fake clock — kept out of this package so a production bundle never pulls the fakes in.

License

MIT © Benoit TRAVERS