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

@hardlydifficult/daemon

v1.0.6

Published

A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.

Readme

@hardlydifficult/daemon

A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.

Installation

npm install @hardlydifficult/daemon

Quick Start

import { runContinuousLoop, createTeardown } from "@hardlydifficult/daemon";

// Setup graceful shutdown
const teardown = createTeardown();
teardown.add(() => console.log("Shutting down..."));
teardown.trapSignals();

// Run a background task every 5 seconds
await runContinuousLoop({
  intervalSeconds: 5,
  runCycle: async (isShutdownRequested) => {
    console.log("Running cycle...");
    if (isShutdownRequested()) return { stop: true };
    return { nextDelayMs: "immediate" };
  },
  onShutdown: async () => {
    await teardown.run();
  }
});

Graceful Shutdown with createTeardown

Manages resource cleanup with LIFO execution order, signal trapping for SIGINT/SIGTERM, and idempotent teardown behavior.

Core API

createTeardown(): Teardown

Creates a teardown manager for registering cleanup functions.

const teardown = createTeardown();

// Add cleanup functions
teardown.add(() => server.close());
teardown.add(() => db.close());

// Or use async functions
teardown.add(async () => {
  await flushPendingWrites();
});

Teardown.add(fn): () => void

Registers a cleanup function. Returns an unregister function for removing it.

const unregister = teardown.add(() => cleanup());

// Later, remove it
unregister();

Teardown.run(): Promise<void>

Runs all cleanup functions in LIFO order. Safe to call multiple times—subsequent calls are no-ops.

await teardown.run(); // Runs last-in-first-out

Teardown.trapSignals(): () => void

Wires SIGINT/SIGTERM to automatically call run() then process.exit(0).

const untrap = teardown.trapSignals();

// Later, restore default behavior
untrap();

Behavior Notes

  • Errors in teardown functions are caught and logged, allowing remaining functions to complete.
  • Signal handlers are added only once per process and cleaned up automatically when untrap() is called.

Continuous Loop with runContinuousLoop

Runs a recurring task in a loop with built-in signal handling, dynamic delays, and configurable error policies.

Core Options

| Option | Type | Description | |--------|------|-------------| | intervalSeconds | number | Default delay between cycles in seconds | | runCycle | () => Promise<RunCycleResult> | Main function executed per cycle | | getNextDelayMs | () => Delay \| undefined | Optional custom delay resolver | | onCycleError | ErrorHandler | Custom error handling strategy | | onShutdown | () => Promise<void> | Cleanup hook called on shutdown | | logger | ContinuousLoopLogger | Optional logger (defaults to console) |

Return Values from runCycle

You can control loop behavior by returning one of:

  • Delay value: number (ms) or "immediate" to skip delay
  • Control object: { stop?: boolean; nextDelayMs?: Delay }
await runContinuousLoop({
  intervalSeconds: 10,
  runCycle: async () => {
    // Skip delay after this cycle
    return "immediate";
  }
});

Example: Backoff Loop

import { runContinuousLoop } from "@hardlydifficult/daemon";

type Result = { backoffMs: number } | { success: true };

await runContinuousLoop({
  intervalSeconds: 60,
  runCycle: async () => {
    const success = await attemptTask();
    return success 
      ? { success: true } 
      : { backoffMs: Math.min(60_000, Math.random() * 5_000) };
  },
  getNextDelayMs: (result) => 
    "backoffMs" in result ? result.backoffMs : undefined,
  onShutdown: () => console.log("Stopping loop"),
});

Error Handling

By default, cycle errors are logged to console and the loop continues. Custom error handling:

await runContinuousLoop({
  intervalSeconds: 5,
  runCycle: async () => { throw new Error("fail"); },
  onCycleError: async (error, context) => {
    console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
    return "stop"; // or "continue"
  }
});

Shutdown Signals

The loop responds to SIGINT and SIGTERM by stopping after the current cycle completes. Use isShutdownRequested() inside runCycle to abort long-running work:

await runContinuousLoop({
  intervalSeconds: 1,
  runCycle: async (isShutdownRequested) => {
    while (!isShutdownRequested()) {
      await processChunk();
    }
    return { stop: true };
  }
});