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

@hex-di/runtime

v0.3.0

Published

Runtime layer for HexDI - immutable containers, scope hierarchy, and type-safe service resolution

Readme

@hex-di/runtime

Runtime container layer for HexDI. Consumes validated dependency graphs produced by @hex-di/graph and turns them into immutable, type-safe containers that resolve and lifetime-manage services.

Overview

@hex-di/runtime sits at the execution boundary of the HexDI stack:

@hex-di/core   – ports, adapters, service interfaces
@hex-di/graph  – dependency graph builder & validation
@hex-di/runtime – container factory, resolution, scopes   ← this package

It provides:

  • Immutable containers – frozen objects created from a validated graph
  • Lifetime management – singleton, scoped, and transient instance caching
  • Scope hierarchy – per-request or per-operation isolation on top of a root container
  • Child containers – inherit or override parent registrations, with sync, async, and lazy loading variants
  • Result-based APIstryResolve / tryResolveAsync / tryDispose return Result/ResultAsync rather than throwing
  • Resolution hooksbeforeResolve / afterResolve callbacks for observability and tracing
  • Container inspection – runtime snapshots of adapters, singletons, and scope trees

Installation

pnpm add @hex-di/runtime @hex-di/core @hex-di/graph

Peer dependencies: Node.js >= 18, TypeScript >= 5.0 (optional but recommended)

Quick Start

import { port, createAdapter } from "@hex-di/core";
import { GraphBuilder } from "@hex-di/graph";
import { createContainer } from "@hex-di/runtime";

// 1. Define ports (interfaces)
interface Logger {
  log(message: string): void;
}
interface Database {
  query(sql: string): Promise<unknown[]>;
}

const LoggerPort = port<Logger>()({ name: "Logger" });
const DatabasePort = port<Database>()({ name: "Database" });

// 2. Define adapters (implementations)
const LoggerAdapter = createAdapter({
  provides: LoggerPort,
  requires: [],
  lifetime: "singleton",
  factory: () => ({ log: msg => console.log(msg) }),
});

const DatabaseAdapter = createAdapter({
  provides: DatabasePort,
  requires: [LoggerPort],
  lifetime: "singleton",
  factory: deps => ({
    query: async sql => {
      deps.Logger.log(`Query: ${sql}`);
      return [];
    },
  }),
});

// 3. Build the graph
const graph = GraphBuilder.create().provide(LoggerAdapter).provide(DatabaseAdapter).build();

// 4. Create a container
const container = createContainer({ graph, name: "App" });

// 5. Resolve services – fully type-safe
const logger = container.resolve(LoggerPort); // Logger
const db = container.resolve(DatabasePort); // Database

logger.log("ready");
const rows = await db.query("SELECT 1");

// 6. Dispose when done (runs finalizers in LIFO order)
await container.dispose();

Core Concepts

Container Phases

Containers start in an "uninitialized" phase. If the graph contains adapters with async factories, call initialize() before resolving them synchronously:

// Async adapter (no explicit lifetime → async)
const DatabaseAdapter = createAdapter({
  provides: DatabasePort,
  requires: [],
  factory: async () => {
    const conn = await openConnection();
    return { query: conn.query.bind(conn) };
  },
});

const container = createContainer({ graph, name: "App" });

// Option A – initialize first, then resolve synchronously
const initialized = await container.initialize();
const db = initialized.resolve(DatabasePort);

// Option B – always use resolveAsync (works regardless of phase)
const db = await container.resolveAsync(DatabasePort);

tryInitialize() returns a ResultAsync instead of throwing.

Scopes

Scopes provide per-operation lifetime isolation. Services registered with lifetime: "scoped" are created fresh per scope and disposed when the scope is disposed.

const RequestContextPort = port<RequestContext>()({ name: "RequestContext" });

const RequestContextAdapter = createAdapter({
  provides: RequestContextPort,
  requires: [],
  lifetime: "scoped",
  factory: () => ({ requestId: crypto.randomUUID() }),
});

// Handle a request
const scope = container.createScope("request-123");
const ctx = scope.resolve(RequestContextPort); // fresh instance per scope
// singleton services resolve to the same instance as the parent container
const logger = scope.resolve(LoggerPort);

await scope.dispose(); // ctx is finalized; singletons are untouched

Scopes can be nested:

const childScope = scope.createScope("nested");

Child Containers

Child containers add or override adapters on top of a parent container. They are always in the "initialized" phase.

const MockLoggerAdapter = createAdapter({
  provides: LoggerPort,
  requires: [],
  lifetime: "singleton",
  factory: () => ({ log: vi.fn() }),
});

const childGraph = GraphBuilder.create().provide(MockLoggerAdapter).build();
const child = container.createChild(childGraph, { name: "Test" });

const mockLogger = child.resolve(LoggerPort); // uses MockLoggerAdapter
const db = child.resolve(DatabasePort); // delegates to parent

Async and lazy child containers

// Async – graph loaded via dynamic import before use
const pluginContainer = await container.createChildAsync(
  () => import("./plugin-graph").then(m => m.pluginGraph),
  { name: "Plugin" }
);

// Lazy – graph not loaded until first resolve()
const lazyPlugin = container.createLazyChild(
  () => import("./plugin-graph").then(m => m.pluginGraph),
  { name: "LazyPlugin" }
);
console.log(lazyPlugin.isLoaded); // false
const svc = await lazyPlugin.resolve(PluginPort); // triggers load
console.log(lazyPlugin.isLoaded); // true

Inheritance Modes

When creating a child container, control how each inherited port behaves:

const child = container.createChild(childGraph, {
  name: "Child",
  inheritanceModes: {
    Logger: "shared", // share parent's singleton instance (default)
    Cache: "isolated", // create a new instance via the same factory
    Config: "forked", // shallow-clone the parent's instance (requires clonable: true)
  },
});

Override Builder

Use container.override() for fluent, type-checked adapter replacement (useful in tests):

const MockLogger = createAdapter({
  provides: LoggerPort,
  requires: [],
  lifetime: "singleton",
  factory: () => ({ log: vi.fn() }),
});

const testContainer = container.override(MockLogger).build(); // returns a child container with the override applied

The type system rejects overrides for ports not in the graph or adapters with unsatisfied dependencies.

Resolution Hooks

Hooks are called synchronously on every resolution, enabling tracing and observability:

const container = createContainer({
  graph,
  name: "App",
  hooks: {
    beforeResolve: ctx => {
      console.log(`Resolving ${ctx.portName} (depth ${ctx.depth})`);
      if (ctx.isCacheHit) console.log("  cache hit");
    },
    afterResolve: ctx => {
      console.log(`Resolved ${ctx.portName} in ${ctx.duration}ms`);
    },
  },
});

Hooks can also be added and removed after container creation:

const handler = ctx => tracer.record(ctx);
container.addHook("afterResolve", handler);
// later…
container.removeHook("afterResolve", handler);

Result-Based APIs

All throwing methods have non-throwing counterparts that return Result / ResultAsync from @hex-di/result:

// Sync resolution
const result = container.tryResolve(LoggerPort);
if (result.isOk()) {
  result.value.log("hello");
} else {
  console.error(result.error.code); // ContainerError
}

// Async resolution
const asyncResult = await container.tryResolveAsync(DatabasePort);

// Disposal
const disposeResult = await container.tryDispose();
if (disposeResult.isErr()) {
  for (const cause of disposeResult.error.causes) {
    console.error(cause);
  }
}

resolveResult and recordResult

Helpers for integrating @hex-di/result adapters with containers:

import { resolveResult, recordResult } from "@hex-di/runtime";

// Resolve a port and return a Result<T, ResolutionError>
const result = resolveResult(() => container.resolve(SomePort));

// Record a Result outcome to an inspector for tracking statistics
const tryResult = container.tryResolve(SomePort);
recordResult(container.inspector, "SomePort", tryResult);

Container Inspection

The inspect() function returns a frozen snapshot of container state:

import { inspect } from "@hex-di/runtime";

const snapshot = inspect(container);
console.log(snapshot.kind); // "root" | "child" | "lazy" | "scope"
console.log(snapshot.singletons); // cached singleton entries
console.log(snapshot.containerName); // human-readable container name
console.log(snapshot.isDisposed); // whether the container has been disposed

The container.inspector property provides a richer event-based API used by devtools integrations.

Error Reference

All errors extend ContainerError and carry a stable code string for programmatic handling.

| Error class | code | isProgrammingError | When thrown | | ---------------------------------- | ---------------------- | -------------------- | ------------------------------------------------- | | CircularDependencyError | CIRCULAR_DEPENDENCY | true | Cycle detected in dependency graph | | FactoryError | FACTORY_FAILED | false | Sync factory threw during resolution | | AsyncFactoryError | ASYNC_FACTORY_FAILED | false | Async factory rejected | | AsyncInitializationRequiredError | ASYNC_INIT_REQUIRED | true | Sync resolve of async port before initialize() | | ScopeRequiredError | SCOPE_REQUIRED | true | Scoped port resolved from root container | | DisposedScopeError | DISPOSED_SCOPE | true | Resolution from a disposed scope/container | | NonClonableForkedError | NON_CLONABLE_FORKED | true | forked inheritance mode on non-clonable adapter | | DisposalError | DISPOSAL_FAILED | false | One or more finalizers threw during disposal |

import {
  ContainerError,
  CircularDependencyError,
  FactoryError,
  DisposedScopeError,
} from "@hex-di/runtime";

try {
  container.resolve(SomePort);
} catch (error) {
  if (error instanceof CircularDependencyError) {
    console.error("Cycle:", error.dependencyChain.join(" -> "));
  } else if (error instanceof FactoryError) {
    console.error("Factory failed for:", error.portName, error.cause);
  } else if (error instanceof ContainerError) {
    console.error(error.code, error.isProgrammingError);
  }
}

Type Utilities

import type {
  InferContainerProvides, // extract the Port union a container provides
  InferScopeProvides, // same, for a Scope
  IsResolvable, // boolean type: can Port P be resolved from Container C?
  ServiceFromContainer, // extract the service type for a given port
} from "@hex-di/runtime";

Context Variables

For propagating ambient context (e.g. request IDs, tenant IDs) through the resolution graph:

import {
  createContextVariableKey,
  getContextVariable,
  setContextVariable,
  getContextVariableOrDefault,
} from "@hex-di/runtime";

const RequestIdKey = createContextVariableKey<string>("requestId");

setContextVariable(ctx, RequestIdKey, "req-abc-123");
const requestId = getContextVariable(ctx, RequestIdKey); // string

Captive Dependency Prevention

The type system enforces that adapters never depend on shorter-lived services. This is checked at the @hex-di/graph layer, with the relevant types re-exported here for completeness:

import type { IsCaptiveDependency, ValidateCaptiveDependency } from "@hex-di/runtime";

Lifetime levels: singleton (longest) > scoped / request > transient (shortest). A singleton may not depend on a scoped or transient service.

Package Exports

| Export path | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------- | | @hex-di/runtime | Public API – use this in application code | | @hex-di/runtime/internal | Implementation classes for sibling packages (@hex-di/react, etc.) – do not use in application code |

Related Packages

| Package | Role | | ---------------- | ---------------------------------------------------- | | @hex-di/core | Port and adapter definitions | | @hex-di/graph | Dependency graph builder and validation | | @hex-di/result | Result / ResultAsync type used by try* methods |

License

MIT