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

dirge

v1.0.5

Published

Hierarchical resource management for JavaScript. Clean up subscriptions, event listeners, and disposables with nested scopes that match your component lifecycle.

Readme

dirge

A lightweight, hierarchical resource management system for JavaScript. Handles cleanup of subscriptions, event listeners, timers, and any other disposable resources in a structured, scope-based manner.

Features

  • 🎯 Universal cleanup: Works with any cleanup pattern (functions, Symbol.dispose, etc.)
  • 🌲 Hierarchical scopes: Organize resources in a tree structure
  • 🔄 Correct disposal order: Children dispose first, local resources in reverse
  • 🪶 Lightweight: No dependencies, minimal API surface
  • 🎨 Flexible: Use nested scopes or path-based lookup

Installation

npm i dirge

Quick Start


import { ManagedResourceScope } from 'dirge';

class Component {
  #disposal = new ManagedResourceScope('component');

  constructor() {
    // Add any cleanup function
    this.#disposal.add(() => console.log('cleanup!'));

    // Subscriptions
    this.#disposal.add(() => signal.unsubscribe());

    // Event listeners
    const handler = () => {};
    this.#disposal.add(() => {
      button.removeEventListener('click', handler);
    });

    // AbortController
    const controller = new AbortController();
    this.#disposal.add(() => controller.abort());

    // Timer
    const timer = setInterval(() => {}, 1000);
    this.#disposal.add(() => clearInterval(timer));
  }

  destroy() {
    this.#disposal.dispose(); // Cleans everything
  }
}

Hierarchical Scopes

Organize related resources into nested scopes:

import { ManagedResourceScope } from 'dirge';

class App {
  #disposal = new ManagedResourceScope('app');

  constructor() {
    // Main app resources
    this.#disposal.add(() => console.log('app cleanup'));

    // UI scope
    const ui = this.#disposal.scope('ui');
    ui.add(() => console.log('ui cleanup'));

    // Modal within UI
    const modal = ui.scope('modal');
    modal.add(() => console.log('modal cleanup'));

    // Keep reference for later
    this.modalScope = modal;
  }

  closeModal() {
    // Dispose only the modal
    this.modalScope.dispose();
  }

  destroy() {
    // Disposes all: modal → ui → app (depth-first)
    this.#disposal.dispose();
  }
}

API

Constructor

const scope = new ManagedResourceScope(name);

Methods

add(resource)

Add a resource to be disposed. Resource must be either:

  • A function: () => cleanup()
  • An object with Symbol.dispose method
scope.add(() => subscription.unsubscribe());
scope.add(() => controller.abort());

scope(name)

Create or get a child scope. Returns a ManagedResourceScope.

const childScope = scope.scope('child');
childScope.add(() => console.log('child cleanup'));

lookup(path)

Get a nested scope by path. Less type-safe than keeping references.

const modal = scope.lookup('ui/modal');
// Equivalent to: scope.scope('ui').scope('modal')

dispose()

Dispose all resources in this scope and all children. Disposal order:

  1. All child scopes (recursively)
  2. Local resources (in reverse order of addition)
scope.dispose();

Properties

path (getter)

Get the full path of this scope from root.

const modal = root.scope('ui').scope('modal');
console.log(modal.path); // "/ui/modal"

parent

Reference to parent scope (or undefined for root).

children

Map of child scopes: Map<string, ManagedResourceScope>

Patterns

Component Lifecycle

import { ManagedResourceScope } from 'dirge';

class Component {
  #disposal = new ManagedResourceScope('component');

  mount() {
    this.#disposal.add(() => element.remove());
    this.#disposal.add(() => signal.unsubscribe());
  }

  unmount() {
    this.#disposal.dispose();
  }
}

Temporary Subscriptions

showDialog() {
  const dialogScope = this.#disposal.scope('dialog');

  dialogScope.add(() => overlay.remove());
  dialogScope.add(() => controller.abort());

  // Later...
  dialogScope.dispose(); // Just the dialog
}

Fetch with Cleanup

async fetchData() {
  const fetchScope = this.#disposal.scope('fetch');
  const controller = new AbortController();

  fetchScope.add(() => controller.abort());

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    return await response.json();
  } finally {
    fetchScope.dispose();
  }
}

Debugging

Visualize your disposal tree:

DOM Visualization

// DOM visualization
export function visualizeDisposalTree(scope, rootName = "root") {
  const ul = document.createElement("ul");
  ul.style.fontFamily = "monospace";
  ul.style.lineHeight = "1.6";

  function buildTree(scope, parentUl) {
    const li = document.createElement("li");
    li.innerHTML = `<strong>${scope.name || rootName}</strong> ` + `<span style="color: #666;">(${scope.size} resources)</span>`;
    parentUl.appendChild(li);

    if (scope.children.size > 0) {
      const childUl = document.createElement("ul");
      li.appendChild(childUl);
      for (const child of scope.children.values()) {
        buildTree(child, childUl);
      }
    }
  }

  buildTree(scope, ul);
  return ul;
}

// Usage
document.body.appendChild(visualizeDisposalTree(myScope));

Console Visualization

export function logDisposalTree(scope, rootName = "root", indent = "") {
  const name = scope.name || rootName;
  console.log(`${indent}├─ ${name} (${scope.size} resources)`);

  const children = Array.from(scope.children.values());
  children.forEach((child, i) => {
    const isLast = i === children.length - 1;
    const newIndent = indent + (isLast ? "   " : "│  ");
    logDisposalTree(child, child.name, newIndent);
  });
}


// Usage
logDisposalTree(myScope);
// Output:
// ├─ app (2 resources)
// │  ├─ ui (1 resources)
// │     ├─ modal (3 resources)

Why Not Just Arrays?

The real problem emerges when components have different lifecycle scopes - persistent resources that live for the component's lifetime, and temporary resources that change as the user navigates through states.

The Problem: Nested Component Lifecycles

Consider a login component with multiple stages:

class AuthComponent {
  #componentScope; // Lives until user logs in
  #userFormScope;  // Recreated when switching login/forgot password

  initialize() {
    // Component-level: survives form switches
    this.#componentScope = app.disposer.scope('auth-component');

    // Subscribe to user profile signal - lives entire component lifetime
    this.#componentScope.add(() => userProfile.unsubscribe());

    // Setup form validation - component-level
    this.#componentScope.add(() => validator.destroy());
  }

  start() {
    // Show login form
    this.showLoginForm();
  }

  showLoginForm() {
    // Clear old form scope if exists
    this.#userFormScope?.dispose();

    // Create fresh scope for this form
    this.#userFormScope = this.#componentScope.scope('loginForm');

    // Form-specific subscriptions
    this.#userFormScope.add(() => {
      loginButton.removeEventListener('click', this.handleLogin);
    });
    this.#userFormScope.add(() => loginFormSignal.unsubscribe());

    // Nested scope for dynamic validation messages
    const validationScope = this.#userFormScope.scope('validation');
    validationScope.add(() => clearValidationUI());
  }

  showForgotPasswordForm() {
    // Automatically cleans loginForm + all its nested scopes
    this.#userFormScope?.dispose();

    // Fresh scope for forgot password
    this.#userFormScope = this.#componentScope.scope('forgotForm');

    this.#userFormScope.add(() => {
      resetButton.removeEventListener('click', this.handleReset);
    });
    this.#userFormScope.add(() => forgotFormSignal.unsubscribe());
  }

  terminate() {
    // Cleanup everything: component scope + any active form scope
    // All nested scopes cleaned automatically
    this.#componentScope.dispose();
  }
}

With Arrays: Chaos

// ❌ Array approach - error prone and messy
class AuthComponent {
  #componentCleanups = [];
  #formCleanups = [];

  initialize() {
    this.#componentCleanups.push(() => userProfile.unsubscribe());
    this.#componentCleanups.push(() => validator.destroy());
  }

  showLoginForm() {
    // Manual cleanup - easy to forget
    this.#formCleanups.forEach(fn => fn());
    this.#formCleanups = [];

    // Add new cleanups
    this.#formCleanups.push(() => loginButton.removeEventListener(...));
    this.#formCleanups.push(() => loginFormSignal.unsubscribe());

    // Wait, where do validation cleanups go?
    // Need another array? #validationCleanups?
    // What about nested wizard steps? #wizardStepCleanups?
    // This gets messy fast...
  }

  showForgotPasswordForm() {
    // Remember to cleanup, every time, manually
    this.#formCleanups.forEach(fn => fn());
    this.#formCleanups = [];

    this.#formCleanups.push(() => resetButton.removeEventListener(...));
    // Easy to forget cleanups or mix up arrays
  }

  terminate() {
    // Cleanup in correct order? Hope you remembered everything
    this.#formCleanups.forEach(fn => fn());
    this.#componentCleanups.forEach(fn => fn());
    // Did we cleanup validation? Nested scopes? Who knows!
  }
}

With ManagedResourceScope: Clear Structure

// ✅ ManagedResourceScope - organized and automatic
class AuthComponent {
  #componentScope;
  #userFormScope;

  initialize() {
    this.#componentScope = app.disposer.scope('auth-component');
    this.#componentScope.add(() => userProfile.unsubscribe());
    this.#componentScope.add(() => validator.destroy());
  }

  showLoginForm() {
    // One line - cleans everything nested inside
    this.#userFormScope?.dispose();

    this.#userFormScope = this.#componentScope.scope('loginForm');
    this.#userFormScope.add(() => loginButton.removeEventListener(...));

    // Nest as deep as needed - cleanup is automatic
    const validation = this.#userFormScope.scope('validation');
    validation.add(() => clearValidationUI());

    const wizard = validation.scope('wizard-step-2');
    wizard.add(() => wizardCleanup());
    // All cleaned when userFormScope.dispose() is called
  }

  terminate() {
    // One line - guarantees everything is cleaned
    // componentScope → userFormScope → validation → wizard-step-2
    // All disposed in correct order (depth-first)
    this.#componentScope.dispose();
  }
}

The Key Insight

Arrays force you to manually track and cleanup parallel hierarchies. Scopes give you a tree structure that matches your component's actual lifecycle.

When you call componentScope.dispose():

  • loginForm scope is disposed first (and all its children)
  • Then componentScope's own resources are disposed
  • You can trust every nested scope is cleaned up - no manual tracking needed

This is especially critical in:

  • Multi-step wizards - each step creates/destroys scopes
  • Tabbed interfaces - tabs have different resource lifetimes
  • Dynamic forms - fields appear/disappear based on user input
  • Modal/dialog chains - modals can spawn other modals

With arrays, you'd need separate arrays for each level and manual coordination. With scopes, the tree structure does the work for you.

License

MIT