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

@asaidimu/utils-artifacts

v8.2.25

Published

Reactive artifact container.

Readme

@asaidimu/utils-artifacts

Reactive dependency injection and lifecycle management for TypeScript applications.

License: MIT

Table of Contents

  1. Overview
  2. Core Concepts
  3. Quick Start
  4. Usage
  5. Architecture
  6. Development
  7. Testing
  8. API Reference

1. Overview

@asaidimu/utils-artifacts is a high-performance TypeScript library for managing the lifecycle, dependencies, and reactivity of application components, referred to as artifacts.

Unlike traditional Dependency Injection (DI) containers, this system implements a Lazy-Resolution, Lazy-Invalidation model. This ensures that components are created only when requested and are rebuilt only when their specific dependencies change, minimizing unnecessary re-computations and avoiding the manual orchestration of event listeners in the UI layer.

2. Core Concepts

To use the library effectively, it is essential to understand the mental model of an artifact's lifecycle:

The Artifact Lifecycle

An artifact transitions through the following states:

  • Idle: The artifact is registered but has not yet been resolved. No resources are consumed.
  • Pending: The factory function is currently executing (asynchronous resolution).
  • Ready: The artifact instance is available and cached.
  • Invalidated: A dependency change has occurred. The cached instance is marked as stale, but the artifact is not rebuilt immediately. It returns to the Idle state for the next resolve() call.

Singleton vs. Transient

  • Singleton: A single instance is shared across the entire container. Ideal for services, API clients, and shared state managers.
  • Transient: A fresh instance is created on every resolve() call. Ideal for lightweight utility functions or state transformations.

3. Quick Start

import { ReactiveDataStore } from "@asaidimu/utils-store";
import { ArtifactContainer } from "@asaidimu/utils-artifacts";

interface State {
  count: number;
}

interface Registry {
  counterLabel: string;
}

const store = new ReactiveDataStore<State>({ count: 0 });
const container = new ArtifactContainer<Registry, State>(store);

// Register a reactive artifact
container.register({
  key: "counterLabel",
  factory: async ({ use }) => {
    const count = await use((ctx) => ctx.select((s) => s.count));
    return `The current count is ${count}`;
  },
});

// Resolve the artifact
const result = await container.resolve("counterLabel");
console.log(result.instance); // "The current count is 0"

// watch the artifact
const observer = container.watch("counterLabel");
observer.subscribe((result) => {
  console.log(`Observed: ${result.instance}`); // "The current count is 0"
});

// simulate state changes
while (true) {
  const { count } = store.get();
  if (count === 10) {
    break;
  }

  await new Promise((r) => setTimeout(r, 300));
  store.set(({ count }) => ({ count: count + 1 }));
}

// TODO
// add example operations for invalidate and calling cleanup
// add more examples for complex artifact interactions

4. Usage

Registering Artifacts

Artifacts are registered via an ArtifactTemplate. By default, artifacts are singletons and lazily instantiated.

container.register({
  key: "apiClient",
  factory: () => new ApiClient(),
  scope: "singleton",
  lazy: true,
});

Resolving Dependencies

Use the ctx.use callback to declare dependencies on other artifacts or state slices. This is the mandatory method for the container to track the dependency graph and trigger invalidations.

container.register({
  key: "userService",
  factory: async ({ use }) => {
    // Dependency on another artifact
    const client = await use((ctx) => ctx.require("apiClient"));
    // Dependency on a slice of the global state
    const config = await use((ctx) => ctx.select((s) => s.authConfig));

    return new UserService(client, config);
  },
});

Streaming Artifacts

Singletons can emit multiple values over time. The container automatically notifies all dependents and observers upon every emission, triggering a cascading invalidation.

container.register({
  key: "socketData",
  factory: ({ stream }) => {
    stream(({ emit, signal }) => {
      const socket = new WebSocket("...");
      socket.onmessage = (e) => emit(e.data);

      // IMPORTANT: Always use the signal to stop the stream
      // when the artifact is invalidated or unregistered.
      signal.addEventListener("abort", () => {
        socket.close();
      });

      // If the stream is a loop, check signal.aborted in every iteration
      // while (!signal.aborted) {
      //   await someAsyncWork();
      //   emit(result);
      // }
    });
    return null; // Initial value
  },
});

Parameterized Artifacts

Define a paramKey to create distinct instances based on resolution parameters.

Critical Resource Management Parameterized singletons are cached indefinitely by default to ensure reactive consistency. In high-cardinality scenarios (e.g., resolving thousands of unique user IDs), this can lead to memory exhaustion over long application runtimes.

Best Practices for Resource Management:

  1. Prefer transient scope: If the artifact does not need to be shared across multiple consumers, use scope: 'transient' to avoid cache accumulation.
  2. Manual Cleanup: For singleton parameterized artifacts, invoke container.unregister(key, params) when the artifact is no longer needed (e.g., when a user logs out or a view is destroyed).
  3. Monitor Cache: If resolving dynamic, high-volume data, track the number of active parameter keys to ensure the container remains within memory constraints.

Critical Note: The paramKey function must be deterministic. The string returned by this function is the actual primary key used for caching and invalidation. If the function is non-deterministic or produces collisions, you will either lose caching benefits or accidentally invalidate unrelated instances of the same parameterized artifact.

container.register({
  key: "userProfile",
  paramKey: (params) => `user:${params.id}`,
  factory: async (ctx) => {
    const { id } = ctx.params;
    return fetchUser(id);
  },
});

// Resolves the cached singleton for user:123
const user = await container.resolve("userProfile", { id: "123" });

Lifecycle Management

Explicitly manage the lifecycle of an artifact by using onCleanup and onDispose within the factory.

container.register({
  key: "connection",
  factory: async ({ onCleanup, onDispose }) => {
    const conn = new Connection();
    await conn.connect();

    onCleanup(() => conn.disconnect());
    onDispose(() => conn.destroy());

    return conn;
  },
});

Observing Artifacts

Use ArtifactObserver (via container.watch) to bind the artifact's state to UI components or external monitors.

const observer = container.watch("userProfile", { id: "123" });

observer.subscribe((resolved) => {
  console.log("Artifact state changed:", resolved.ready);
  if (resolved.ready) {
    console.log("Instance available:", resolved.instance);
  }
});

Persistence: Export and Import

The container can export its current set of built singleton artifacts into a serializable bundle and later restore them in a new container. This is useful for saving/reloading application state, or transferring a snapshot across environments.

Exporting Artifacts

Call container.export() to generate a JSON‑compatible bundle.

  • The method waits for all currently resolving artifacts to finish.
  • Only singleton artifacts that have been fully built (i.e., ready === true) are included.
  • Each exported artifact stores:
    • Its resolved instance (must be JSON‑serializable).
    • The state groups (store selectors) it depends on, with their paths and options.
    • Its list of artifact dependencies (other artifact keys).
  • The bundle contains a checksum to detect corruption.
const bundle = await container.export();
// {
//   version: "1.0",
//   timestamp: 1700000000000,
//   checksum: "abc123...",
//   artifacts: [
//     {
//       key: "config",
//       instance: { theme: "dark" },
//       state: { groups: [{ paths: ["theme"], options: undefined }] },
//       dependencies: []
//     },
//     // ...
//   ]
// }

Note: Transient artifacts are never exported. If an artifact instance contains non‑serializable values (e.g., functions, symbols), export() will throw an error.

Importing Artifacts

Use the static ArtifactContainer.from() method to create a new container pre‑filled with exported data.

  • You must provide:
    • A store (the same or a different ReactiveDataStore).
    • The previously exported bundle.
    • The original artifact templates (the container does not store factory functions).
  • The method validates the bundle’s checksum. On mismatch, it throws "Bundle checksum mismatch".
  • For each exported artifact, the container restores the instance only if all its state groups still match the current store’s data. If any selected slice has changed, the artifact is marked stale (not restored) and will be rebuilt on the next resolve().
// Create a new container from a bundle
const restoredContainer = await ArtifactContainer.from({
  store: new ReactiveDataStore(initialState),
  bundle: savedBundle,
  templates: [
    {
      key: "config",
      factory: async ({ use }) => ({
        theme: await use(({ select }) => select((s: any) => s.theme)),
      }),
      scope: "singleton",
    },
    {
      key: "userName",
      factory: async ({ use }) =>
        `User-${await use(({ select }) => select((s: any) => s.userId))}`,
      scope: "singleton",
    },
    // ... other templates
  ],
});

Staleness and Reactivity

When importing, the container compares the exported state groups (store selectors) with the current store:

  • If all state group values are identical → the artifact instance is restored directly (no rebuild).
  • If any value differs → the artifact is skipped (not restored) and its cache entry remains empty. The next resolve() will run the factory again, using the latest store data.

Staleness propagates through the dependency graph: if artifact A depends on B and B is stale, then A is also considered stale and will be rebuilt.

// Example: store changed from { theme: "dark", userId: 42 } to { theme: "light", userId: 42 }
// - config (depends on theme) → stale, not restored
// - userName (depends on userId) → fresh, restored
// - greeting (depends on config) → stale (transitive), not restored

Parameterized Artifacts

Parameterized singletons are exported and imported per parameter key. The same staleness checks apply independently for each parameter combination.

container.register({
  key: "user",
  paramKey: (params) => `user:${params.userId}`,
  factory: async (ctx) => ({ id: ctx.params.userId, name: "..." }),
  scope: "singleton",
});

// Export includes both user:alice and user:bob
const bundle = await container.export();

// Later, restore both instances
const newContainer = await ArtifactContainer.from({ store, bundle, templates });
newContainer.peek("user", { userId: "alice" }); // restored instance
newContainer.peek("user", { userId: "bob" }); // restored instance

Complete Example

import { ReactiveDataStore } from "@asaidimu/utils-store";
import { ArtifactContainer } from "@asaidimu/utils-artifacts";

// 1. Initial setup
const store = new ReactiveDataStore({ theme: "dark", userId: 42 });
const container = new ArtifactContainer(store);

container.register({
  key: "greeting",
  factory: async ({ use }) => {
    const theme = await use(({ select }) => select((s: any) => s.theme));
    const userId = await use(({ select }) => select((s: any) => s.userId));
    return `${theme} greeting for user ${userId}`;
  },
  scope: "singleton",
  lazy: false,
});

await container.resolve("greeting"); // builds "dark greeting for user 42"

// 2. Export the bundle
const bundle = await container.export();

// 3. Later, in a different environment (e.g., another request)
const newStore = new ReactiveDataStore({ theme: "light", userId: 42 });
const restored = await ArtifactContainer.from({
  store: newStore,
  bundle,
  templates: [
    /* same greeting template */
  ],
});

// theme changed → greeting is stale, will be rebuilt on demand
const greeting = await restored.require("greeting");
console.log(greeting); // "light greeting for user 42"

Important Notes

  • The exported bundle is read‑only and should be treated as an immutable snapshot.
  • Always provide all necessary templates when calling from(). Missing templates cause resolution errors.
  • For high‑cardinality parameterized artifacts, consider cleaning up unused keys before exporting to keep the bundle size reasonable.
  • The checksum prevents accidental corruption but does not provide cryptographic security.

Container Extension

Containers support a parent-child extension model. When a child extends a parent, it can transparently resolve artifacts defined in the parent — no proxy entries or local caching of parent instances.

const store = new ReactiveDataStore({});
const parent = new ArtifactContainer(store);
parent.register({ key: "db", factory: () => new Database() });

const child = new ArtifactContainer(new ReactiveDataStore({}));
const unextend = child.extend(parent);

// Resolves from parent's registry — no local proxy entry created
const db = await child.require("db");

// Read-only methods delegate transparently
child.has("db"); // true
child.peek("db"); // Database instance (if resolved)
child.watch("db"); // Parent's observer

// Un-extend when done
unextend();

Resolution order: local registry first, then parents in the order they were extended.

Invalidation propagation: when a parent artifact is invalidated, an event propagates to the child. The child's dependency graph is checked for any local artifacts that depend on that parent key, and those dependents are cascade-invalidated. Observers on the child side are notified and re-resolve from the parent automatically.

Export behavior: child.export() only includes artifacts registered and built locally. Parent artifacts are exported from the parent — each container owns its own export bundle.

Read-only methods (has, peek, watch, invalidate) delegate to the parent transparently when the key is not found locally:

  • has(key) — checks local registry, then each parent in order
  • peek(key) — checks local cache, then each parent's cache
  • watch(key) — returns the parent's watcher directly
  • invalidate(key) — delegates to the parent's invalidate() when the key is only in a parent

Lifecycle: extend() returns a cleanup function. Calling it removes the parent, unsubscribes from invalidation events, and disconnects the resolution chain. container.dispose() cleans up all parent subscriptions automatically.

Extending with a ScopedContainer

extend() also accepts a ScopedContainer. This wires the child's resolution chain and invalidation propagation through the scoped container's underlying parent. Since key mangling happens at the ScopedContainer layer, the child must use the mangled key to resolve scoped artifacts directly:

const container = new ArtifactContainer(new ReactiveDataStore({}));
const scoped = container.scope("tenant");
scoped.register({ key: "config", factory: () => "tenant-config" });

const child = new ArtifactContainer(new ReactiveDataStore({}));
child.extend(scoped);

// Child resolves global artifacts normally
await child.require("config"); // from container

// Child resolves scoped artifacts via the mangled key
await child.require("tenant::config"); // "tenant-config"

Invalidation events from the ScopedContainer propagate to the child, so changes to scoped artifacts trigger rebuilds in the child's dependents. The cleanup function works identically — call it to disconnect.

Scoped Containers

A scoped container is a lightweight view of an existing container that namespaces every registered artifact under a scope prefix. It does not create a new container — all artifacts live in the same registry, cache, and dependency graph as the parent.

This is useful for multi-tenant scenarios, request-scoped state, or any situation where the same artifact key should resolve to different instances depending on the scope.

const container = new ArtifactContainer(store);

// Register a global config
container.register({
  key: "config",
  factory: () => ({ url: "https://global.com" }),
});

// Create two scoped views
const tenantA = container.scope("acme");
const tenantB = container.scope("megacorp");

// Each scope registers its own config
tenantA.register({
  key: "config",
  factory: () => ({ url: "https://acme.com" }),
});
tenantB.register({
  key: "config",
  factory: () => ({ url: "https://megacorp.com" }),
});

// Each scope resolves its own config
(await tenantA.resolve("config")).instance.url; // "https://acme.com"
(await tenantB.resolve("config")).instance.url; // "https://megacorp.com"

// The global config remains untouched
(await container.resolve("config")).instance.url; // "https://global.com"

Key Mangling

Every artifact registered on a scoped container is stored under a mangled key in the parent container: <scope>::<key>. For example, tenantA.register({ key: "api" }) stores the artifact as "acme::api" in the parent.

Differentiation from parameterized keys: The :: separator is reserved for scoped keys. Parameterized keys use user-defined formats (e.g., user:42), so there is no ambiguity.

Uniqueness

Scope names must be unique per container. Calling container.scope("acme") twice throws:

container.scope("acme"); // OK
container.scope("acme"); // Error: Scope "acme" already exists

Resolution Fallback

When resolving a key, the scoped container tries the scoped key first. If the scoped key is not found, it falls back to the global key. This applies to resolve, require, has, peek, and invalidate:

const scoped = container.scope("myscope");

// myscope::config exists locally → resolved
await scoped.resolve("config");

// myscope::db does not exist → falls back to global "db"
await scoped.require("db");

Dependency Resolution Inside Factories

When a scoped artifact's factory uses ctx.use(), the resolve and require calls inside the callback try the scoped key first, then fall back to the global key. This means scoped artifacts naturally compose with each other:

container.register({ key: "baseUrl", factory: () => "https://global.com" });

const scoped = container.scope("myapp");
scoped.register({ key: "baseUrl", factory: () => "https://myapp.com" });
scoped.register({
  key: "api",
  factory: async ({ use }) => {
    // Resolves scoped "myapp::baseUrl" first → "https://myapp.com"
    const url = await use((deps) => deps.require("baseUrl"));
    return new HttpClient(url);
  },
});

If a scoped dependency does not exist, the global fallback kicks in transparently:

scoped.register({
  key: "logger",
  factory: async ({ use }) => {
    // "myapp::db" does not exist → uses global "db"
    const db = await use((deps) => deps.require("db"));
    return new Logger(db);
  },
});

Parent Visibility

Since scoped artifacts are stored in the parent container's registry, the parent can discover them via debugInfo() or by resolving the mangled key directly:

container.debugInfo().find((n) => n.id.startsWith("acme::"));
// → { id: "acme::config", scope: "singleton", ... }

await container.resolve("acme::config");
// → Resolves the acme-scoped config directly

Watcher Identity

scoped.watch(key) returns the same ArtifactObserver object for repeated calls with the same key, preserving the watcher identity guarantee:

const a = scoped.watch("config");
const b = scoped.watch("config");
a === b; // true

API

ScopedContainer mirrors a subset of ArtifactContainer:

| Method | Behaviour | | :-------------------------- | :-------------------------------------------------------------------------------------------------------- | | register(template) | Registers the artifact under the mangled key. Wraps paramKey and factory for scoped-first resolution. | | resolve(key, params?) | Resolves the scoped key, falling back to the global key. | | require(key, params?) | Returns the resolved instance or throws. Same fallback semantics. | | has(key) | Returns true if the scoped or global key exists. | | peek(key, params?) | Returns the cached instance from the scoped cache, then global cache. | | invalidate(key, options?) | Invalidates the scoped key, falling back to the global key. | | unregister(key, params?) | Unregisters the scoped artifact. | | watch(key, params?, ttl?) | Returns an ArtifactObserver for the mangled key. Identity-preserving. | | notifyObservers(key) | Notifies observers of the mangled key. | | hasWatchers(key) | Checks if the mangled key has active watchers. |

ScopedContainer satisfies the ContainerLike interface and can be passed to another container's extend() method. See Extending with a ScopedContainer.

import { ScopedContainer } from "@asaidimu/utils-artifacts";
// or via container.scope()

5. Architecture

@asaidimu/utils-artifacts implements a Directed Acyclic Graph (DAG) for dependency management using a Pull-based Invalidation model.

Troubleshooting Circular Dependencies

Circular dependencies occur when artifact A depends on B, and B depends on A, creating a cycle in the DAG. The container will detect this at runtime and throw a SystemError.

Common Remediation Strategies:

  1. Extract Shared Logic: Identify the common dependency shared by both artifacts and move it into a third, independent base artifact.
  2. Use Provider Pattern: Refactor the shared behavior into a 'provider' artifact that both A and B can depend on without forming a loop.
  3. Decompose Artifacts: Break the circular logic into smaller, discrete units where one unit coordinates the interaction between the others.

Push vs. Pull Reactivity

In traditional reactive systems (Push model), a change in a source value immediately triggers a chain reaction of re-computations across all dependents. This often leads to "update storms" where a single state change causes dozens of immediate, synchronous re-renders or calculations, regardless of whether the result is actually needed at that moment.

In contrast, this library uses a Pull model. When a state slice or source artifact emits a change, the container simply marks all downstream dependent artifacts as Invalidated (setting a stale flag). It does not execute any factories. No work is performed until a consumer actually calls resolve() or an observer is notified.

This approach ensures that if a state value changes ten times in a single event loop, the dependent artifacts are only rebuilt once—the next time they are requested. This collapses multiple invalidations into a single, deferred rebuild, maximizing computational efficiency and reducing main-thread blocking.

6. Development

# Install dependencies
bun install

# Run unit tests
bun test

# Run tests in watch mode
bun run test:watch

7. Testing

The library is verified against a comprehensive suite including:

  • Resolution race condition prevention.
  • Circular dependency detection.
  • Circular dependency detection.
  • Stream lifecycle and AbortSignal compliance.
  • Parameterized invalidation cascades.

8. API Reference

ArtifactContainer

| Method | Description | | :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | register(template) | Registers a new artifact template. | | scope(name) | Creates a ScopedContainer that namespaces all registered artifacts under "<name>::". Scope names must be unique. Throws on duplicate. | | extend(parent) | Extends this container with a parent container or ScopedContainer. Returns a cleanup function. Read-only methods (has, peek, watch) transparently access parent artifacts. Invalidation propagates from parent to child. No proxy entries — parent artifacts are resolved directly from the parent's cache. | | resolve(key, params?) | Resolves the artifact. Returns a ResolvedArtifact object containing the instance and ready status. | | require(key, params?) | Same as resolve, but returns the instance directly or throws if resolution if resolution fails. | | invalidate(key, params?) | Manually trigger a rebuild of the artifact and its dependents. | | unregister(key, params?) | Removes the artifact from the registry. If params is omitted, the entire template is removed; if provided, only the specific virtual artifact instance is removed. |

ArtifactObserver

| Method | Description | | :---------------------------- | :---------------------------------------------------------------------------------------------------------- | | get() | Returns the current cached snapshot of the artifact. | | subscribe(callback, eager?) | Subscribes to changes. The callback is invoked whenever the artifact state changes (Ready, Error, Pending). | | resolve() | Manually triggers the resolution of the artifact if it is currently idle. |

ArtifactFactoryContext

| Method | Description | | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | | use(callback) | The primary method for declaring dependencies. Any resolve or select calls inside the callback are registered as dependencies. | | onCleanup(cleanup) | Registers a function to be executed before the artifact is rebuilt or disposed. | | onDispose(callback) | Registers a function to be executed be executed before the artifact is permanently removed. | | stream(callback) | Initializes a streaming process for a Singleton artifact. | | state() | Returns a non-reactive read of the current global state. |

ArtifactTemplate

| Method | Description | | :--------- | :---------------------------------------------------------------------- | | key | Unique identifier for the artifact. | | factory | The factory function that creates the artifact instance. | | scope | singleton (default) or transient. | | lazy | If true (default), the artifact is built only when requested. | | timeout | Maximum time allowed for the factory to complete. | | retries | Number of times to retry the factory on failure. | | debounce | Base debounce time in milliseconds for invalidation events to collapse. | | paramKey | Function to generate a unique key for parameterized artifacts. |

ResolvedArtifact

| Property | Description | | :--------- | :----------------------------------------------------------------------------------- | | instance | The resolved artifact instance. Available only when ready is true. | | ready | Boolean indicating if the artifact is resolved and available for use. | | error | The error caught during the factory execution. Available only when ready is false. |

Error Handling & Recovery

Since ResolvedArtifact is a discriminated union, you can safely narrow the type of the instance or the error by checking the ready flag:

const artifact = await container.resolve("userProfile");

if (artifact.ready) {
  // Type is narrowed to ReadyArtifact: instance is available
  console.log(artifact.instance);
} else {
  // Type is narrowed to ErrorArtifact: error is available
  console.error("Failed to resolve artifact:", artifact.error);

  // Optionally trigger a manual rebuild to recover from transient failures
  // await container.invalidate("userProfile");
}

ArtifactStreamContext

| Method | Description | | :------------ | :-------------------------------------------------------- | | emit(value) | Emits a new value, triggering invalidation of dependents. | | set(update) | Dispatches a state update to the global store. | | signal | AbortSignal to stop the stream. |

ScopedContainer

| Method | Description | | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | The scope name provided at creation. | | manager | The parent container's ArtifactManager. Enables extend() support. | | events | The parent container's event bus. Enables invalidation propagation through extend(). | | register(template) | Registers the artifact under the mangled key "<name>::<key>". Factory use() resolves scoped dependencies first, then falls back. | | resolve(key, params?) | Resolves the scoped key; if not found, falls back to the global key. | | require(key, params?) | Like resolve but returns the instance directly or throws. | | has(key) | Checks scoped key first, then global key. | | peek(key, params?) | Returns cached instance from scoped cache, then global cache. | | invalidate(key, options?) | Invalidates the scoped key, falling back to the global key. | | unregister(key, params?) | Unregisters the scoped artifact. | | watch(key, params?, ttl?) | Returns an ArtifactObserver for the mangled key. Identity-preserving — repeated calls return the same observer. | | notifyObservers(key) | Notifies observers for the mangled key. | | hasWatchers(key) | Checks if the mangled key has active watchers. | | dispose() | Unregisters all scoped artifacts (including parameterized virtual instances) and frees the scope name for reuse. Idempotent. All other methods throw after disposal. |

Disposal

Call scoped.dispose() to clean up all artifacts registered through the scope. This:

  • Unregisters every artifact (including parameterized virtual instances) whose mangled key starts with "<name>::"
  • Removes the scope name from the container, allowing a new scope with the same name to be created
  • Is idempotent — calling dispose() multiple times is safe
  • Renders the ScopedContainer unusable — any subsequent method call throws 'Scope "<name>" has been disposed.'
const scoped = container.scope("tenant");
scoped.register({ key: "config", factory: () => ({}) });
scoped.register({
  key: "user",
  paramKey: (p) => `user:${p.id}`,
  factory: ({ params }) => fetchUser(params!.id),
});

await scoped.resolve("user", { id: 1 });

// Clean up everything
await scoped.dispose();

// The scope name can be reused
const fresh = container.scope("tenant");