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-strategies

v1.0.0

Published

A fail-closed, zero-GC strategy router over a booted DI container: a gate maps an input to a declared key, the key resolves to a construction-frozen token, and select()/resolve() add no allocation and no catch. Read-only by construction -- it never mutate

Readme

@zakkster/lite-di-strategies

A fail-closed, zero-GC STRATEGY ROUTER over a booted DI container. Declare a map of routing KEYS to container TOKENS and a GATE that turns a runtime input into a key; select(input) returns the token, resolve(input) is exactly container.get(select(input)). One gate call, one Map lookup, one === undefined compare, one return -- no string building, no try/catch, no allocation on the cached-singleton lane. READ-ONLY by construction: the router never mutates the container. Zero deps, single file.

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

The dispatch the DI ecosystem was missing

lite-di-strategies is the DISPATCH hinge of the @zakkster/lite-di-* service-kernel line. The container OWNS resolution and hot-swap, the supervisor HEALS, health REPORTS, the orchestrator RETIRES on SIGTERM, the lock guards a critical section. Nothing turned a runtime signal -- a tenant tier, a feature flag, a payload kind -- into "which of N registered implementations do I resolve?" without a hand-rolled switch that quietly drifts from the container's registrations. This is that piece, and it stays a pure SELECTOR.

npm install @zakkster/lite-di-strategies
import { Container } from '@zakkster/lite-di-container';
import { StrategyRouter } from '@zakkster/lite-di-strategies';

const c = new Container();
c.singleton('plan:free', FreePlan);
c.singleton('plan:pro',  ProPlan);
c.singleton('plan:ent',  EnterprisePlan);
c.boot();

// strategies: { key -> token }; gate: (input) -> key. Both validated at construction.
const plans = new StrategyRouter(c, {
  strategies: { free: 'plan:free', pro: 'plan:pro', ent: 'plan:ent' },
  gate: (user) => user.tier,
});

const plan = plans.resolve(currentUser);   // == c.get(the token for user.tier)

One gate call, one Map lookup, one compare. An undeclared key THROWS -- never a silent default. The router calls get and has and nothing else, ever: it is boot-lock-clean by construction, and 0 B/op on the cached-singleton lane.


Table of contents


Why this exists

The Strategy pattern in a DI app usually degrades into a switch on some runtime value that returns a hand-picked instance. That switch is a SECOND registry: it drifts from the container's real registrations, it is not fail-closed (an unknown case falls through to a default or undefined), and it allocates on a path you call per request.

StrategyRouter replaces the switch with a lens onto the container's ONE registry. A gate maps your input to a declared key; the key resolves to a precomputed token; the token resolves through container.get. There is no second source of truth, no silent fallthrough, and no allocation on the hot lane.

What you get

  • A pure SELECTOR: select(input) -> token, resolve(input) -> instance. Nothing else.
  • Fail closed EVERYWHERE: construction validates the container, the options, the gate, and every token (container.has(token) === true, naming key AND token on failure). A gate that returns an undeclared key throws; gate: undefined is never coerced.
  • 0 B/op on the cached-singleton lane, gated two ways in the torture harness.
  • Boot-lock-clean by construction: the router never mutates the container. Proven by a mutator-spy total of exactly 0 across a million selects.
  • resolve adds NO catch: the container's own errors propagate byte-for-byte.
  • Zero runtime dependencies. Single ESM file. TypeScript types.

The selector, in one read

select(input) {
  const key = this._gate(input);            // your gate: input -> key
  const token = this._tokens.get(key);      // precomputed key -> token Map
  if (token === undefined) throw this._unknownKey(key);   // cold builder, off the hot body
  return token;
}

resolve(input) {
  return this._container.get(this.select(input));         // no try/catch, no wrapping
}

Tokens are precomputed into a construction-frozen Map, so the hot body does NO string building. The single === undefined compare is the only branch; it covers a typo, null, undefined, a number, an object, or NaN in one shot -- any of which throws a TypeError from the COLD _unknownKey builder (which lists the declared keys and adds a Levenshtein did-you-mean). resolve wraps nothing: if container.get throws, that error is what you catch, unchanged.

API reference

new StrategyRouter(container, options)

new StrategyRouter(container, {
  strategies: { [key]: token },   // exactly one shape; two keys MAY map to one token
  gate: (input) => key,           // required; undefined is NEVER coerced to match-all
})

container is duck-typed: it must expose get(token), has(token), and an isBooted boolean, and isBooted must be true. Construction fails closed, before any read, on each of:

| Rejected at construction | Result | | --- | --- | | container non-object / null | TypeError | | container missing get / has | TypeError | | container missing isBooted boolean | TypeError | | isBooted === false | Error (build a router only over a booted container) | | options non-object / null | TypeError | | an unknown option key | Error -- unknown option 'strategis'. Did you mean 'strategies'? | | gate missing / non-function | TypeError | | gate: undefined | TypeError -- never coerced to match-all | | strategies missing / empty | TypeError / Error | | a token not a non-empty string or symbol | TypeError | | a token where container.has(token) !== true | Error naming the KEY and the TOKEN |

Two keys mapping to one token is LEGAL and documented.

select and resolve

| Member | Signature | Notes | | --- | --- | --- | | select | (input) => token | HOT. Throws a TypeError (with a did-you-mean over the declared keys) if the gate returns an undeclared key. Never false, never a silent no-op. A gate that throws propagates unchanged. | | resolve | (input) => instance | HOT. == container.get(select(input)). Adds no catch: the container's throws propagate byte-for-byte. | | keys | ReadonlyArray | Frozen array of the declared keys, built once at construction. | | size | number | The number of declared keys. |

resolve re-throws, unchanged, the container's own errors -- Service 'x' is not registered. Available: [...], 'x' is multi. Use getAll()., 'x' is async. Use getAsync()., Container shut down.

Constants

| Export | Value | | --- | --- | | VERSION | '1.0.0' (synced with package.json + llms.txt) |

No default export. No bind helper. No frozen enum -- every rejection is a thrown Error.

Composability with the ecosystem

The router selects; the container swaps. They compose without merging. To change WHICH implementation a token resolves to at runtime, call container.rebind BESIDE the router -- the router makes zero API calls, yet its next resolve returns the new instance:

import { Container, TYPES } from '@zakkster/lite-di-container';
import { StrategyRouter } from '@zakkster/lite-di-strategies';

const parent = new Container();
parent.singleton('impl:free', FreePlan);
parent.singleton('impl:pro',  ProPlan);
parent.singleton('impl:ent',  EnterprisePlan);
parent.boot();

// A booted child scope with no local impl binding -- resolution delegates to parent.
const scope = parent.scope();
scope.boot();

const router = new StrategyRouter(scope, {
  strategies: { free: 'impl:free', pro: 'impl:pro', ent: 'impl:ent' },
  gate: (ctx) => ctx.tier,
});

router.resolve({ tier: 'pro' });   // ProPlan, resolved through the child scope

// Hot-swap the pro plan live -- the router is NOT involved.
await parent.rebind('impl:pro', { type: TYPES.VALUE, value: new ProPlanV2(), isAsync: false });

router.resolve({ tier: 'pro' });   // ProPlanV2 -- zero router API calls

rebind lives at Container.js:1138 and is orthogonal to selection (see decisions/0001-selector-not-rebinder.md).

Zero-GC design notes

The hot lanes are measured, not claimed. In the torture harness, select and resolve are driven 1,000,000 times each across TWO routers over TWO containers with disjoint key sets and rotating input (so nothing is constant-folded), gated two ways: measureAllocs at maxBytesPerCall: 0, and measureOps(stabilize:'deep') with checkNoGc({ maxMajor: 0 }), maxPauseMs <= 2.0, and arrayBuffers.growthBytes === 0.

| Lane | What runs | Bytes/op | | --- | --- | --- | | select -> token | one gate call, one Map lookup, one compare, return | 0.000 | | resolve -> instance, CACHED-SINGLETON token (Container.js:203) | the above + container.get cache hit | 0.000 | | resolve -> instance, VALUE / ALIAS token (Container.js:228-237) | the above + the container's path-tracking lane (_path push/pop, indexOf) | NOT 0 -- out of the gated claim |

Two honest caveats:

  1. The gated 0 B/op number is for the CACHED-SINGLETON lane. Register your strategies as singleton / singletonFactory and resolve them once to warm the cache, and resolve rides Container.js:203 at 0 bytes. value and alias tokens take the container's path-tracking lane, which allocates and can grow -- those are deliberately OUT of the 0 B/op table.
  2. 0 B/op is a JOINT property of the router AND your gate. The router's own body allocates nothing, but select calls YOUR gate on every op. If the gate allocates (builds a string key, spreads an object, closes over fresh state), that allocation is on the select path and the number degrades. The router cannot make an allocating gate free; it only promises to add nothing of its own.

The declared-keys Map and the frozen keys array are built ONCE at construction. The _unknownKey error builder and the Levenshtein did-you-mean are COLD -- reached only on a reject branch, never from a passing select.

Design decisions worth knowing

  • The router is a SELECTOR, not a REBINDER (decision 0001). It only ever calls get and has. Hot-swap is the container's job via rebind; selection and swap compose without merging.
  • Fail closed, always. gate: undefined is a MISSING gate, not a match-all. An undeclared key throws, never returns a default -- if you want a default, make the gate return a declared key (your policy, in one place).
  • One options shape only. strategies is exactly { [key]: token } -- no prefix, separator, array sugar, or bitmask. The single === undefined compare is the only hot branch.
  • Tokens precomputed at construction. The hot body does no string building; every token is validated container.has(token) === true up front.

Testing

Three entry points:

npm test        # the node:test smoke suite
npm run torture # node --expose-gc test/torture.mjs -- prints "ok", one GATE line, exit 0
npm run example # the runnable reference app -- self-verifying, exits 0 on OK

examples/billing-dispatch.mjs is a shipped, self-verifying reference consumer: a billing service that routes each request's tenant tier to one of three registered plan implementations, then swaps the pro plan live with container.rebind BESIDE the router (selection and swap compose without merging). It exercises the whole public surface -- select / resolve / .keys / .size / VERSION, the fail-closed throw on an undeclared key, and the hot-swap -- and asserts every contract with node:assert, so npm run example is part of verify and prepublishOnly. It is the downstream proof that the 1.0.0 API works in anger.

The torture harness runs eight tiers strictly sequentially: laws + exports + an ASCII / no-console control (fail-closed over every files[] entry); the 16-row construction matrix; the >= 14-row select/resolve fail-closed matrix (container messages byte-identical); a boot-lock proof (a million selects leave the registry / resolution order / singleton cache byte-identical, mutator spies at exactly 0); the 0 B/op select + resolve lanes; a 10,000-cycle retention soak with lite-leak; a money-tier composition against the REAL container (three tenants + scope delegation + a rebind beside the router); and controls that prove every gate above can fail. Three break switches -- DI_ASCII_BREAK, DI_ALLOC_BREAK, DI_TORTURE_BREAK -- each force a non-zero exit, so no gate is vacuous.

What this is not

  • NOT a rebinder. It never mutates the container. Use container.rebind for hot-swap.
  • NOT a registry. It is a lens onto the container's registrations, not a second store.
  • NOT a fallback engine. An undeclared key throws; a default is an explicit gate choice.
  • NOT a DI container. It requires a booted @zakkster/lite-di-container (or any object exposing get / has / isBooted).

Ecosystem

Part of the @zakkster/lite-di-* service-kernel line: the container resolves and hot-swaps, the supervisor heals, health reports, the orchestrator retires on SIGTERM, the lock guards a critical section -- and this package dispatches a runtime signal to the right registered implementation.

License

MIT (c) Zahary Shinikchiev [email protected]