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

@zakkster/lite-di-supervisor

v1.0.0

Published

OTP-style supervision tree over a @zakkster/lite-di-container topology: declared children restarted on fault by one-for-one / one-for-all / rest-for-one strategy via the container's invalidate/rebind primitives, with a restart-intensity budget that escala

Downloads

31

Readme

@zakkster/lite-di-supervisor

OTP-style supervision tree over a @zakkster/lite-di-container topology. A Supervisor owns a declared, ordered set of container TOKENS and restarts them on an EXPLICITLY-PUSHED fault by one-for-one / one-for-all / rest-for-one strategy, via the container's invalidate + getAsync primitives, with a restart-intensity budget that escalates instead of hot-looping. The healthy poll path -- check() -- allocates 0 bytes/op, hard-gated.

npm version sponsor Zero-GC npm bundle size npm downloads npm total downloads Tree-Shakeable TypeScript Dependencies license

The supervision tree the DI line was missing

@zakkster/lite-di-container already builds, wires, and tears down your object graph, and (since 2.2.0, decision 0005) exposes invalidate(token) -- flush one cached instance so the next resolve rebuilds it from the SAME registration. What it deliberately does NOT own is the POLICY on top of that primitive: when a subsystem faults, WHICH children restart, in WHAT order, and how many times before you stop trying. The container gives you the atomic per-token restart; it stays out of the ordering decision on purpose.

lite-di-supervisor is that missing policy layer -- an OTP-style supervision tree. It owns a declared, ordered set of container tokens (its children). When a fault is PUSHED to it, it restarts the affected children by one of three strategies via invalidate + an eager getAsync re-resolve, so no live instance is ever left holding a torn-down dependency. A restart-intensity budget (maxRestarts within windowMs, against an INJECTED now()) escalates instead of hot-looping. This is the GAP-1 keystone that turns a wired container into a self-healing service kernel.

npm install @zakkster/lite-di-supervisor

Peer dependency (not bundled, install it alongside):

npm install @zakkster/lite-di-container   # >=2.2.0 (the invalidate primitive)
import { Container } from '@zakkster/lite-di-container';
import { Supervisor, STRATEGIES } from '@zakkster/lite-di-supervisor';

class Db    { async connect() { /* open pool */ } }
class Cache { constructor(db) { this.db = db; } }
class Api   { constructor(cache) { this.cache = cache; } handle(req) { /* ... */ } }

const c = new Container();
c.singleton('db', Db);
c.singleton('cache', Cache, ['db']);
c.singleton('api', Api, ['cache']);
c.boot();

const sup = new Supervisor(c, {
  children: ['db', 'cache', 'api'],   // declared START order
  strategy: STRATEGIES.REST_FOR_ONE,  // faulting 'cache' rebuilds cache + api
});
await sup.start();                    // resolve children in order, arm supervision

// A subsystem method threw? Push the fault -- the supervisor heals the subtree:
await sup.reportFault('cache', new Error('redis dropped'));
// cache and api are rebuilt from their registrations; db is untouched.

sup.check();   // 0 -- all children healthy again (0 B/op readout)

Table of contents

Why this exists

A DI container and a self-healing supervision tree solve two halves of the same problem and, until now, never met. The container constructs and wires your subsystems and can flush and rebuild any single one of them (invalidate) -- but a thrown error inside a subsystem's own method is INVISIBLE to it, and it has no opinion on which OTHER subsystems a fault should take down with it. It ships the atomic per-token primitive and stops there, by design: container decision 0005 states plainly that transitive restart ordering (one-for-one, one-for-all, rest-for-one) is the future supervisor's responsibility.

This package IS that responsibility. It adds the four things the container deferred: a fault-reporting CONTRACT (faults are pushed, never guessed), a restart-ordering POLICY (the three strategies), a restart-intensity BUDGET (stop hot-looping and escalate), and an escalation SEAM (surface a dead subtree to whatever sits above). The container gives the healing primitive; the supervisor owns when and how far the healing spreads.

What you get

  • A Supervisor over a container and a declared, ordered set of child TOKENS -- first-class container citizens, resolved in start order at start(), restarted through the container's own public invalidate + getAsync.
  • Three restart strategies -- one-for-one (heal just the faulted token), one-for-all (shared-fate: rebuild every child), rest-for-one (a dependency chain: rebuild the faulted token and everything started after it).
  • An EXPLICIT-PUSH fault contract: reportFault(token, err?) is the primitive; run(token, fn) wraps a call so a throw self-reports and then re-throws. No polling, no reflection, no timers, no magic method-wrapping.
  • A restart-intensity budget with escalation: maxRestarts within windowMs, measured against an INJECTED now(), so the budget is fully deterministic in a test. Exhaustion fires onEscalate once -- it never hot-loops.
  • A 0-allocation readiness poll: check() returns the count of non-healthy children in a single loop over an SoA Uint8Array -- no now(), no closure, no array. Hard-gated at 0.000 B/op. This is the tick a long-running server calls.
  • Fail-closed behavior end to end: an unknown option throws with a did-you-mean hint, a not-booted or under-registered container throws at start() and stays INIT (atomic), and a dead supervisor (ESCALATED / SHUT_DOWN) never restarts.

Full types ship in Supervisor.d.ts.

The fault contract

The container cannot see an error thrown inside a subsystem's own method: a rejected db.query() or a throwing cache.get() is invisible to the graph that built db and cache. So the supervisor never tries to GUESS a fault -- it is TOLD. There are exactly two ways in, and both are explicit:

  • reportFault(token, err?) is the primitive. You caught (or observed) a fault in a subsystem; you name the token that failed. The supervisor marks that child non-healthy SYNCHRONOUSLY (so check() reflects it on the very next call), then applies the restart policy on a serialized cold lane.
  • run(token, fn) is the convenience wrapper. It runs fn; on success it returns the value; on throw it calls reportFault(token, e) and then RE-THROWS the original error. The caller ALWAYS sees its own error -- the supervisor heals in the background but never swallows. Wrap a request handler or a work-loop body with it and a throw becomes a self-report for free.
// Primitive: you own the try/catch, you name the token.
try {
  await cache.warm();
} catch (e) {
  await sup.reportFault('cache', e);   // heal, then decide what to tell the caller
}

// Wrapper: the throw self-reports, and the caller still gets the error.
const rows = await sup.run('db', () => db.query(sql));  // on throw: restart db, re-throw

What is NOT the fault channel: check(). It is a health READOUT -- the count of children currently marked non-healthy -- and it never restarts, never reads now(), and never allocates. A poll cannot see a fault the subsystem did not surface, so polling is not how faults arrive; it is only how you ask "is everything healthy right now?" on a hot loop. Faults come in through the two push doors above, and nowhere else.

API reference

Constructor

new Supervisor(container: Container, spec: {
  children: (string | symbol)[];   // declared START order; non-empty, unique
  strategy?: 'one-for-one' | 'one-for-all' | 'rest-for-one';  // default 'one-for-one'
  maxRestarts?: number;            // integer >= 1; default 3
  windowMs?: number;               // finite number > 0; default 5000
  now?: () => number;              // default monotonic real clock
  onRestart?: (info) => void;      // fired after a successful restart
  onEscalate?: (info) => void;     // fired once (terminally) on budget exhaustion
})
  • container -- a booted-by-start() @zakkster/lite-di-container instance (>=2.2.0). A null / non-object container is a TypeError.
  • children -- the declared START order, a non-empty array of unique string | symbol tokens, each registered on the container by the time start() runs. Empty, duplicate, or non-token children throw.
  • strategy -- one of the three STRATEGIES values (default 'one-for-one'). An unknown strategy throws, naming the three valid values.
  • maxRestarts -- the intensity budget's count, an integer >= 1 (default 3). Anything else is a TypeError.
  • windowMs -- the intensity window in milliseconds, a finite number > 0 (default 5000).
  • now -- the clock the budget is measured against, () => number (default a monotonic real clock: performance.now() when present, else Date.now()). Inject a scriptable clock for a fully deterministic budget in tests. Read ONLY on the cold restart path, never in check().
  • onRestart(info) -- optional; fired AFTER each successful restart with { token, index, strategy, error } (error is null when none was passed).
  • onEscalate(info) -- optional; fired ONCE, terminally, when the budget is exhausted, with { token, restarts, windowMs }.

An UNKNOWN option key throws with a did-you-mean hint (unknown option 'stratergy'. Did you mean 'strategy'?) -- it is never silently ignored. Every bad type throws in the constructor; nothing is armed until start().

Methods

start(): Promise<void>
check(): number
reportFault(token: string | symbol, err?: unknown): Promise<void>
run<T>(token: string | symbol, fn: () => T | Promise<T>): Promise<T>
shutdown(): Promise<void>
get state(): number
  • start() -- resolve every child in declared start order (via getAsync, so async singletons are supported) and arm supervision (INIT -> RUNNING). Only from INIT; the container must be booted (isBooted) and every child registered (has), else it throws and STAYS INIT. Validation is atomic -- all checks run BEFORE any resolution, so a bad topology never leaves you half-armed.
  • check(): number -- HOT. The count of non-healthy children (0 == all healthy). A single loop over an SoA Uint8Array; no now(), no closure, no array, no allocating branch. Allowed in RUNNING and ESCALATED (a readout is always safe); returns 0 in INIT / SHUT_DOWN (nothing armed). 0.000 B/op.
  • reportFault(token, err?): Promise<void> -- record a fault and apply the restart policy. Marks the child non-healthy synchronously (so check() reflects it immediately), then restarts SERIALIZED behind any in-flight restart. Only while RUNNING; an unknown token throws; after ESCALATED or SHUT_DOWN it throws (a dead supervisor never restarts). The not-running and unknown-token guards throw synchronously, before the method returns a promise -- so a bare reportFault(t).catch(h) does NOT catch a guard error; it escapes the .catch. Wrap the call in try/catch, or use run() (which reports from inside its own try). Only the restart itself settles through the returned Promise<void>.
  • run(token, fn): Promise<T> -- run fn; on success return its value; on throw call reportFault(token, e) and then RE-THROW the original error (fail-closed: the caller always sees its own error). Only while RUNNING; an unknown token, or a non-function fn, throws before fn runs. If the supervisor escalated between fn throwing and the self-report, that is observable via onEscalate / check() and never masks the caller's error.
  • shutdown(): Promise<void> -- stop supervising (terminal, idempotent). An already-in-flight restart is DRAINED to completion first (aborting mid-restart would leave children half-invalidated); only QUEUED faults are skipped. After SHUT_DOWN, reportFault / run throw.
  • state (getter) -- the current lifecycle state, one of STATES.

Constants

STRATEGIES = { ONE_FOR_ONE, ONE_FOR_ALL, REST_FOR_ONE }   // frozen string enum
STATES     = { INIT, RUNNING, ESCALATED, SHUT_DOWN }       // frozen int enum
VERSION    // version string, three-place-synced with package.json + CHANGELOG

| Export | Type | Value / meaning | | ------------------ | ------------------- | --------------------------------------------------------- | | STRATEGIES.ONE_FOR_ONE | string | 'one-for-one' -- restart only the faulted token. | | STRATEGIES.ONE_FOR_ALL | string | 'one-for-all' -- restart every child (shared fate). | | STRATEGIES.REST_FOR_ONE | string | 'rest-for-one' -- restart the token and all after it. | | STATES.INIT | number (0) | Constructed, not yet started. check() returns 0. | | STATES.RUNNING | number (1) | Armed and supervising. reportFault / run allowed. | | STATES.ESCALATED | number (2) | Budget exhausted; terminal. reportFault / run throw. | | STATES.SHUT_DOWN | number (3) | Stopped; terminal. reportFault / run throw. | | VERSION | string | '1.0.0'. |

Restart ordering, for a faulted token T at start-index i in children = [c0..cn]:

  • one-for-one: invalidate(T), then re-resolve T. Only T.
  • one-for-all: tear down every child in REVERSE-start order (cn..c0) via invalidate, then re-resolve in START order (c0..cn), so every dependent rebuilds against fresh dependencies.
  • rest-for-one: tear down cn..ci in reverse, re-resolve ci..cn in start order. Children before i are untouched -- in a valid start order they cannot depend on T. Eager re-resolution is what resolves the container's 0005 stale-dependent hazard: no live instance is left holding a torn-down dependency.

Composability with the DI line

A full service kernel: subsystems wired in the container, a supervisor armed over them with rest-for-one, a work loop wrapped in run() so a throw self-heals, and an onEscalate that surfaces a dead subtree to an orchestrator.

import { Container } from '@zakkster/lite-di-container';
import { Supervisor, STRATEGIES, STATES } from '@zakkster/lite-di-supervisor';

// --- Subsystems, a dependency chain: pool -> repo -> worker. ---------------
class Pool   { async open() { /* connect */ } async q(sql) { /* ... */ } }
class Repo   { constructor(pool) { this.pool = pool; } find(id) { return this.pool.q('...'); } }
class Worker { constructor(repo) { this.repo = repo; } step(job) { return this.repo.find(job.id); } }

const c = new Container();
c.singleton('pool', Pool);
c.singleton('repo', Repo, ['pool']);
c.singleton('worker', Worker, ['repo']);
c.boot();

// --- Arm supervision over the chain. rest-for-one: a pool fault rebuilds ----
//     pool -> repo -> worker in order; a worker fault rebuilds only worker.
const sup = new Supervisor(c, {
  children: ['pool', 'repo', 'worker'],
  strategy: STRATEGIES.REST_FOR_ONE,
  maxRestarts: 5,
  windowMs: 10000,
  onRestart: (info) => log.warn('healed', info.token, 'via', info.strategy),
  onEscalate: (info) => orchestrator.subtreeDead('jobs', info),  // the seam upward
});
await sup.start();

// --- A work loop. run() wraps each step: a throw self-reports to 'worker' ---
//     (healing the chain) and then re-throws, so the loop can back off.
async function pump(job) {
  try {
    return await sup.run('worker', () => c.get('worker').step(job));
  } catch (e) {
    if (sup.state === STATES.ESCALATED) throw e;  // subtree is dead; stop pumping
    await backoff();                              // healed; retry shortly
  }
}

// --- A readiness endpoint polls the 0 B/op readout every request. ----------
function readiness() { return sup.check() === 0 ? 200 : 503; }

// --- Terminal teardown drains any in-flight restart. -----------------------
await sup.shutdown();

Every stage snaps onto the DI line: the supervisor touches only the container's PUBLIC surface (isBooted, has, getAsync, invalidate) -- no internals, no second copy of the wiring. The container owns the instances and the restart primitive; the supervisor owns the ordering, the budget, and the escalation seam.

Zero-GC design notes

check() is the only hot lane -- the readiness poll a long-running server calls every tick -- so it is gated at exactly zero, not "small". It is a single loop over a pre-allocated SoA Uint8Array of child states, returning the count of non-healthy entries. It reads no wall clock, builds no message, captures no closure, and takes no branch that allocates. Everything else -- reportFault, restart, escalate, shutdown -- is COLD by design: faults are rare, and the serialized restart chain, the injected-now() budget read, and the hook payload objects live there where allocation is fine.

| Path | Steady-state allocations | | -------------------------------------- | ---------------------------------------------- | | check() (poll, N children) | 0.000 B/op (HARD gate, 1e6 ops, 64 children) | | reportFault / restart / escalate | cold by design (rare; allocation is fine) | | shutdown | cold by design (terminal, once) |

The intensity budget itself is 0-alloc to MAINTAIN across a soak: restart timestamps live in a FIXED Float64Array ring of size maxRestarts, so a million fault/restart cycles cannot grow it unbounded. The torture gate (@zakkster/lite-leak + @zakkster/lite-gc-profiler, under --expose-gc) proves both the poll and the soak:

  • check(): 0.000 B/op over 1e6 calls on a 64-child fan-out, maxMajor 0.
  • T7 soak: 100000 fault/restart cycles + 1e6 check() polls, major GCs = 0, retained leak size = 0.

Numbers reproduce with node --expose-gc test/torture.mjs. No gate output is a FAIL.

Design decisions worth knowing

The four load-bearing forks were ratified in decisions/0001-supervision-model.md; each conceptually builds on container decision 0005 (invalidate / rebind).

  • Fault detection is EXPLICIT PUSH (Fork 1). The supervisor learns of a fault only when told, via reportFault (primitive) or run (a wrapper that reports then re-throws). WHY: a poll cannot see a fault a subsystem did not surface, and magic wrapping of every method is neither zero-dep nor deterministic. Push is the only zero-magic, testable contract. check() is a readout, not the channel.
  • The window clock is an INJECTED now() (Fork 2). The budget is measured against a now: () => number (default a monotonic real clock), read ONLY on the cold restart path. WHY: a scriptable clock makes the intensity budget fully deterministic in a test, and no lane ever reads a wall clock directly.
  • Nesting is SINGLE-LEVEL + an escalate hook (Fork 3). Budget exhaustion fires onEscalate(info); it does NOT restart a parent supervisor. WHY: ship the primitive with a clean seam. Multi-level supervisor-of-supervisors is deferred; the escalation hook is where a parent will one day subscribe to a child's death.
  • Restart == invalidate-only (Fork 4). A restart is invalidate(token) (flush + rebuild from the SAME registration) followed by an eager getAsync(token) re-resolve. WHY: the supervisor's job is self-HEALING, not re-wiring. Registration SWAP (rebind, blue/green reconfiguration) is a distinct concern and out of alpha scope. Keeping restart == invalidate makes the stale-dependent handling a pure ORDERING problem -- the three strategies rebuild the affected suffix/set in order, so no live instance is left holding a torn-down dependency (the container's 0005 hazard, resolved here as policy).

Every unverified state fails closed: a not-booted or under-registered container throws at start() and stays INIT (atomic); a budget-exceeded attempt escalates rather than silently restarting once more or hot-looping; a fault arriving mid-restart is chained AFTER it, never recursed, so overlapping invalidate storms on one token cannot occur and the restart order stays deterministic.

Testing

  • npm test -- 103 node:test cases across construction, start/check lifecycle, the three strategies, the intensity budget + escalation, and the run wrapper (including the boundary and fail-closed matrix).
  • npm run torture -- node --expose-gc test/torture.mjs: the 0.000 B/op check() gate over 1e6 calls on a 64-child fan-out (maxMajor 0), and the T7 soak (100000 fault/restart cycles + 1e6 polls, major GCs 0, leak size 0).
  • npm run verify -- npm test then npm run torture, the publish gate.

What this is not

  • Not a multi-level supervisor-of-supervisors. A supervisor's children are container tokens, one level deep. Budget exhaustion surfaces through onEscalate -- that hook is the seam a supervision tree will one day hang off of, but the tree itself is deferred to a later minor.
  • Not a reconfiguration tool. Restart == invalidate (rebuild from the same registration). Registration SWAP / blue-green (rebind) is a separate concern and out of alpha scope. The supervisor heals; it does not re-wire.
  • Not a liveness prober. There are no timers and no polling for health. Faults are PUSHED (reportFault / run); check() is a readout you call, not a probe the supervisor runs.
  • Not a transitive cascade beyond the declared child set. A supervisor restarts ITS OWN children by the chosen ordering; it does not reach across to another supervisor's tokens. Cross-supervisor ordering is the future tree's job.
  • Not a process manager. This restarts DI-constructed subsystems inside one process. For OS processes / containers use pm2, systemd, or kubernetes; for retry/backoff of individual CALLS use a retry library -- this restarts SUBSYSTEMS, not calls.
  • Not the container. Wiring, lifetimes, and the invalidate primitive live in @zakkster/lite-di-container (the peer dependency); the supervisor is the policy on top.

Ecosystem

Part of the @zakkster/lite-di-* line -- a self-healing zero-GC backend service kernel:

  • @zakkster/lite-di-container -- the SPINE: DI wiring, lifetimes, and the invalidate / rebind restart primitive (peer dependency).
  • @zakkster/lite-di-event-bus -- zero-GC DI fan-out / pub-sub (sibling).
  • @zakkster/lite-di-cron -- wall-clock scheduling over the container (sibling).
  • @zakkster/lite-di-ticker -- the per-frame system loop, 0 B/frame (sibling).
  • @zakkster/lite-di-graph -- topology visualization / describe() formatting (sibling).
  • @zakkster/lite-di-supervisor -- this package: the GAP-1 self-healing KEYSTONE that makes the line a service kernel.
  • @zakkster/lite-gc-profiler / @zakkster/lite-leak -- the allocation gate and retention witness that prove the 0 B/op poll and the soak.

License

MIT (c) Zahary Shinikchiev [email protected]