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

@lindorm/worker

v0.5.3

Published

Interval-based background worker with retry, jitter, lifecycle events, graceful shutdown, and structured logging.

Downloads

205

Readme

@lindorm/worker

Interval-based background worker with retry, jitter, lifecycle events, graceful shutdown, and structured logging.

This package is ESM-only. All examples use import syntax — require is not supported.

Installation

npm install @lindorm/worker

@lindorm/logger is a peer dependency — LindormWorker requires an ILogger instance.

Quick Start

import { LindormWorker } from "@lindorm/worker";

const worker = new LindormWorker({
  alias: "cleanup",
  interval: "10m",
  jitter: "30s",
  logger,

  callback: async (ctx) => {
    ctx.logger.info("Running cleanup", { seq: ctx.seq });
    await db.deleteExpired();
  },
});

worker.start();

Subpath Exports

| Export | Purpose | | ------------------------------ | ----------------------------------- | | @lindorm/worker | Main runtime API | | @lindorm/worker/mocks/jest | Jest createMockWorker() factory | | @lindorm/worker/mocks/vitest | Vitest createMockWorker() factory |

Options

const worker = new LindormWorker({
  // Required
  alias: "my-worker", // identifier used in logs
  interval: "5m", // execution interval (ms or readable time)
  callback: async (ctx) => {
    /* ... */
  }, // main work
  logger, // ILogger from @lindorm/logger

  // Optional
  jitter: "30s", // +/- spread around interval (default: 0)
  callbackTimeout: "2m", // abort callback after this duration (default: disabled)
  errorCallback: async (ctx, err) => {
    /* ... */
  }, // runs after all retries are exhausted
  retry: {
    // override retry defaults
    strategy: "exponential", // "exponential" | "linear" | "constant" (default: "exponential")
    timeout: 250, // initial retry delay ms (default: 250)
    timeoutMax: 30_000, // retry delay cap ms (default: 30000)
    maxAttempts: 10, // retries per interval after the initial call (default: 10)
  },
  listeners: [
    // pre-register event listeners
    { event: "error", listener: (err) => alerting.notify(err) },
  ],
});

All time values accept milliseconds or human-readable strings ("30s", "5m", "1h", "1d", "1w").

The constructor throws LindormWorkerError when interval <= 0, jitter < 0, or callbackTimeout < 0.

Lifecycle

start() ──> callback ──> [success] ──> wait(interval +/- jitter) ──> callback ──> ...
                │
                └── [error] ──> retry(1) ──> retry(2) ──> ... ──> retry(maxAttempts)
                        │            │                                    │
                     warning      warning                              warning
                     emitted      emitted                              emitted
                                                                          │
                                                                  retry(maxAttempts + 1)
                                                                          │
                                                                       error
                                                                      emitted
                                                                          │
                                                                   errorCallback
                                                                          │
                                                              wait(interval +/- jitter)

Jitter

Jitter is centred around the base interval: interval + random(-jitter, +jitter). With interval: "10m" and jitter: "30s", the actual wait is between 9m30s and 10m30s.

Retry

When the callback throws, the worker retries with a configurable backoff before moving to the next interval. Each failed attempt up to and including maxAttempts emits a warning event. When the next attempt would exceed maxAttempts, an error event is emitted and the optional errorCallback runs. Errors from errorCallback itself are caught and logged at warn level.

Callback Timeout

When callbackTimeout is set, the callback is raced against a deadline. If the callback exceeds the timeout, a LindormWorkerError with message "Callback timed out" is raised and enters the retry flow.

Events

worker.on("start", () => {
  /* worker started */
});
worker.on("stop", () => {
  /* worker stopped */
});
worker.on("success", () => {
  /* callback succeeded */
});
worker.on("warning", (err) => {
  /* callback failed, retrying */
});
worker.on("error", (err) => {
  /* all retries exhausted */
});

warning and error listeners receive a LindormWorkerError. Use off() to remove a listener and once() for one-shot listeners. After destroy(), on(), off(), and once() throw.

Callback Context

The callback receives a context object:

type LindormWorkerContext = {
  logger: ILogger; // child logger scoped to this execution
  seq: number; // monotonic counter (1, 2, 3, ...)
  latestSuccess: Date | null;
  latestError: Date | null;
  latestTry: Date | null;
};

errorCallback receives the same context plus the error: (ctx, err) => Promise<void>.

Methods

| Method | Description | | ----------- | ----------------------------------------------------------------------------------- | | start() | Begin interval execution. No-op if already started. Throws after destroy(). | | stop() | Graceful shutdown. Awaits any running callback, then stops. | | destroy() | Stop, remove all listeners, mark worker as destroyed. Subsequent calls throw. | | trigger() | Run the callback immediately (outside the interval loop). Throws after destroy(). | | health() | Returns a LindormWorkerHealth snapshot. |

Health Check

const h = worker.health();
// {
//   alias: "cleanup",
//   started: true,
//   running: false,
//   destroyed: false,
//   seq: 42,
//   latestSuccess: Date,
//   latestError: null,
//   latestTry: Date,
// }

State Getters

worker.alias; // string
worker.started; // boolean
worker.running; // boolean — true while the callback is executing
worker.seq; // number — incremented at the start of each interval
worker.latestStart; // Date | null
worker.latestStop; // Date | null
worker.latestSuccess; // Date | null
worker.latestError; // Date | null
worker.latestTry; // Date | null

File Scanner

LindormWorkerScanner.scan() discovers workers from a list of file paths, directory paths, and existing ILindormWorker instances. Directories are scanned recursively. Files matching index.*, *.fixture.*, *.spec.*, *.test.*, or *.integration.* are skipped.

For each scanned file, the scanner first checks for an exported LindormWorker instance (default or named export). If none is found, it falls back to constructing a LindormWorker from CALLBACK-style uppercase exports.

import { LindormWorkerScanner } from "@lindorm/worker";

const workers = await LindormWorkerScanner.scan(
  [
    "./src/workers", // directory, scanned recursively
    existingWorker, // ILindormWorker instances pass through unchanged
  ],
  logger,
);

scan() is async and returns Promise<Array<ILindormWorker>>.

CALLBACK-style File

// workers/CleanupWorker.ts
import type { LindormWorkerCallback } from "@lindorm/worker";
import type { ReadableTime } from "@lindorm/date";
import type { RetryOptions } from "@lindorm/retry";

export const CALLBACK: LindormWorkerCallback = async (ctx) => {
  ctx.logger.info("running cleanup");
};

export const INTERVAL: ReadableTime = "15m";
export const JITTER: ReadableTime = "1m";
export const RETRY: RetryOptions = { maxAttempts: 3, strategy: "linear" };

INTERVAL is required for CALLBACK-style files. The file's basename is used as the worker alias unless an ALIAS export is provided.

Recognised Exports

| Export | Type | Required | | ------------------ | ------------------------------- | -------------------------------------- | | CALLBACK | LindormWorkerCallback | Yes (when no LindormWorker instance) | | INTERVAL | ReadableTime \| number | Yes (when using CALLBACK) | | ALIAS | string | No (defaults to file basename) | | JITTER | ReadableTime \| number | No | | CALLBACK_TIMEOUT | ReadableTime \| number | No | | ERROR_CALLBACK | LindormWorkerErrorCallback | No | | LISTENERS | LindormWorkerListenerConfig[] | No | | RETRY | RetryOptions | No |

Instance-style File

// workers/SessionWorker.ts
import { LindormWorker } from "@lindorm/worker";

export default new LindormWorker({
  alias: "SessionWorker",
  callback: async () => {
    /* ... */
  },
  interval: "30s",
  logger,
});

Either default or named LindormWorker exports are picked up. The first instance found in the file wins.

Errors

| Error | Thrown when | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | LindormWorkerError | Validation failure, callback timeout, or method called after destroy | | LindormWorkerScannerError | Scanner finds a file with neither a LindormWorker instance nor a CALLBACK export, or a CALLBACK file missing INTERVAL |

Both extend LindormError from @lindorm/errors.

Testing

Mock factories are exported under per-runner subpaths so the package itself does not depend on jest or vitest.

// Vitest
import { createMockWorker } from "@lindorm/worker/mocks/vitest";

const mock = createMockWorker();

mock.trigger.mockResolvedValue(undefined);
expect(mock.start).toHaveBeenCalled();
// Jest
import { createMockWorker } from "@lindorm/worker/mocks/jest";

Each factory returns a Mocked<ILindormWorker> (vitest) or jest.Mocked<ILindormWorker> (jest). All methods are typed mock functions; destroy(), stop(), and trigger() resolve to undefined by default.

License

AGPL-3.0-or-later