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

lifecycle-utils

v4.3.1

Published

A set of general utilities for the lifecycle of a JS/TS project/library

Readme

lifecycle-utils

A set of general utilities for the lifecycle of a JS/TS project/library

Build License Types Version codecov

Installation

npm install --save lifecycle-utils

This is an ESM package, so you can only use import to import it, and cannot use require

Documentation

withLock

Calling withLock acquires an exclusive lock for the given scope values, ensuring that its callback cannot run in parallel to any other exclusive or shared lock that use the same scope values.

The order of the values in the scope array is important, and should be consistent across calls to reference the same lock. You can use as many values as you like, but always ensure that at least one of them is a reference to an object.

import {withLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like
const startTime = Date.now();

async function doSomething(index: number): number {
    return await withLock([scope, "myKey"], async () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("index:", index, "time:", Date.now() - startTime);
        return 42;
    });
}

const res = await Promise.all([
    doSomething(1),
    doSomething(2),
    doSomething(3)
]);

// index: 1 time: 1000
// index: 2 time: 2000
// index: 3 time: 3000

console.log(res); // [42, 42, 42]

isLockActive

Check whether a lock is currently active for the given scope values.

import {isLockActive} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

const res = isLockActive([scope, "myKey"]);
console.log(res); // false

acquireLock

Acquire an exclusive lock for the given scope values.

import {acquireLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

const activeLock = await acquireLock([scope, "myKey"]);
console.log("lock acquired");

// ... do some work

activeLock.dispose();

Using the using feature of TypeScript is also supported:

import {acquireLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

{
    using lock = await acquireLock([scope, "myKey"]);
    console.log("lock acquired");
    
    // ... do some work
}

console.log("lock released");

waitForLockRelease

Wait for all locks to be released for a given scope values.

import {waitForLockRelease} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

await waitForLockRelease([scope, "myKey"]);
console.log("lock is released");

withSharedLock

Calling withSharedLock acquires a shared lock for the given scope, allowing multiple shared locks to run in parallel while preventing an exclusive lock from running at the same time.

Lock requests are acquired in the order they are made, with consecutive shared lock requests acquired together. A new shared lock joins an active shared lock immediately when no regular lock requests are waiting.

import {withLock, withSharedLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like
const sleep = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration));

const exclusive1 = withLock([scope, "myKey"], async () => {
    console.log("exclusive 1 started");
    await sleep(1000);
    console.log("exclusive 1 finished");
});

const shared1 = withSharedLock([scope, "myKey"], async () => {
    console.log("shared 1 started");
    await sleep(1000);
    console.log("shared 1 finished");
});
const shared2 = withSharedLock([scope, "myKey"], async () => {
    console.log("shared 2 started");
    await sleep(500);
    console.log("shared 2 finished");
});

const exclusive2 = withLock([scope, "myKey"], async () => {
    console.log("exclusive 2 started");
    await sleep(1000);
    console.log("exclusive 2 finished");
});

await Promise.all([exclusive1, shared1, shared2, exclusive2]);

// exclusive 1 started
// exclusive 1 finished
// shared 1 started
// shared 2 started
// shared 2 finished
// shared 1 finished
// exclusive 2 started
// exclusive 2 finished

acquireSharedLock

Acquire a shared lock for the given scope values.

Multiple shared locks can be held in parallel, while an exclusive lock cannot be held until every shared lock is released.

Lock requests are acquired in the order they are made, with consecutive shared lock requests acquired together. A new shared lock joins an active shared lock immediately when no regular lock requests are waiting.

import {acquireSharedLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

const activeLock = await acquireSharedLock([scope, "myKey"]);
console.log("shared lock acquired");

// ... do some work

activeLock.dispose();

Using the using feature of TypeScript is also supported:

import {acquireSharedLock} from "lifecycle-utils";

const scope = {}; // can be a reference to any object you like

{
    using lock = await acquireSharedLock([scope, "myKey"]);
    console.log("shared lock acquired");
    
    // ... do some work
}

console.log("lock released");

EventRelay

A simple event relay.

Create a listener with createListener and dispatch events with dispatchEvent.

For each supported event type, create a new instance of EventRelay and expose it as a property.

For example, this code:

import {EventRelay} from "lifecycle-utils";

class MyClass {
    public readonly onSomethingHappened = new EventRelay<string>();

    public doSomething(whatToDo: string) {
        this.onSomethingHappened.dispatchEvent(whatToDo);
        console.log("Done notifying listeners");
    }
}

const myClass = new MyClass();
myClass.onSomethingHappened.createListener((whatHappened) => {
    console.log(`Something happened: ${whatHappened}`);
});
myClass.doSomething("eat a cookie");

Will print this:

Something happened: eat a cookie
Done notifying listeners

ScopedEventRelay

A scoped event relay.

Create a listener for a given scope with createListener and dispatch events for a given scope with dispatchEvent.

For each supported event type, create a new instance of ScopedEventRelay and expose it as a property, where the scope identifies the target the event belongs to.

A scope consists of one or more values that identify the target or context of an event, such as a clientId string.

import {ScopedEventRelay} from "lifecycle-utils";

class Clients {
    public readonly onClientMessage = new ScopedEventRelay<[clientId: string], string>();

    public sendMessage(clientId: string, message: string) {
        this.onClientMessage.dispatchEvent([clientId], message);
        console.log("Done notifying listeners");
    }
}

const clients = new Clients();
clients.onClientMessage.createListener(["client1"], (message) => {
    console.log(`Message from client1: ${message}`);
});
clients.onClientMessage.createListener(["client2"], (message) => {
    console.log(`Message from client2: ${message}`);
});
clients.sendMessage("client1", "eat a cookie");
clients.sendMessage("client2", "eat a sandwich");

Will print this:

Message from client1: eat a cookie
Done notifying listeners
Message from client2: eat a sandwich
Done notifying listeners

DisposeAggregator

DisposeAggregator is a utility class that allows you to add multiple items and then dispose them all at once.

You can add a function to call, an object with a dispose method, or an object with a Symbol.dispose method.

To dispose all the items, call dispose or use the Symbol.dispose symbol.

import {DisposeAggregator, EventRelay} from "lifecycle-utils";

const disposeAggregator = new DisposeAggregator();

const eventRelay = new EventRelay<string>();
disposeAggregator.add(eventRelay);

const eventRelay2 = disposeAggregator.add(new EventRelay<string>());

disposeAggregator.dispose();
console.log(eventRelay.disposed === true); // true
console.log(eventRelay2.disposed === true); // true

AsyncDisposeAggregator

AsyncDisposeAggregator is a utility class that allows you to add multiple items and then dispose them all at once. The items are disposed one by one in the order they were added. When the parallel option is enabled, then all the items are disposed in parallel, triggered by the order in which they were added.

You can add a function to call, an object with a dispose method, an object with a Symbol.dispose method, an object with a Symbol.asyncDispose method, or a Promise that resolves to one of the previous types.

To dispose all the items, call dispose or use the Symbol.asyncDispose symbol.

The difference between AsyncDisposeAggregator and DisposeAggregator is that AsyncDisposeAggregator can dispose async targets.

import {AsyncDisposeAggregator, EventRelay} from "lifecycle-utils";

const disposeAggregator = new AsyncDisposeAggregator();

const eventRelay = new EventRelay<string>();
disposeAggregator.add(eventRelay);

disposeAggregator.add(async () => {
    await new Promise(resolve => setTimeout(resolve, 0));
    // do some async work
});

disposeAggregator.dispose();

With the parallel option enabled:

import {AsyncDisposeAggregator, EventRelay} from "lifecycle-utils";

const disposeAggregator = new AsyncDisposeAggregator({parallel: true});

const eventRelay = new EventRelay<string>();
disposeAggregator.add(eventRelay);

disposeAggregator.add(async () => {
    console.log("1");
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log("4");
});
disposeAggregator.add(async () => {
    console.log("2");
    await new Promise(resolve => setTimeout(resolve, 0));
    console.log("3");
});

disposeAggregator.dispose();
// will print:
// 1
// 2
// 3
// 4

DisposableHandle

An object that provides a .dispose() method that can called only once.

Calling .dispose() will call the provided onDispose function only once. Any subsequent calls to .dispose() will do nothing.

import {DisposableHandle} from "lifecycle-utils";

function createHandle() {
    console.log("allocating resources");
    
    return new DisposableHandle(() => {
        console.log("resources disposed");
    });
}

const handle = createHandle();
handle.dispose();

Using the using feature of TypeScript is also supported:

import {DisposableHandle} from "lifecycle-utils";

function createHandle() {
    console.log("allocating resources");
    
    return new DisposableHandle(() => {
        console.log("resources disposed");
    });
}

function doWork() {
    using handle = createHandle();
}

doWork();
// resources disposed
// the dispose function was called since the scope of the `doWork` function ended

AsyncDisposableHandle

An object that provides an async .dispose() method that can called only once.

Calling .dispose() will call the provided onDispose function only once. Any subsequent calls to .dispose() will do nothing.

import {AsyncDisposableHandle} from "lifecycle-utils";

function createHandle() {
    console.log("allocating resources");
    
    return new AsyncDisposableHandle(async () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("resources disposed");
    });
}

const handle = createHandle();
await handle.dispose();

Using the await using feature of TypeScript is also supported:

import {AsyncDisposableHandle} from "lifecycle-utils";

function createHandle() {
    console.log("allocating resources");

    return new AsyncDisposableHandle(async () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("resources disposed");
    });
}

async function doWork() {
    await using handle = createHandle();
}

await doWork();
// resources disposed
// the dispose function was called since the scope of the `doWork` function ended

MultiKeyMap

MultiKeyMap is a utility class that works like a Map, but accepts multiple values as the key for each value.

.set(...), .get(...), .has(...), .delete(...) are in time complexity of O(1), given that the length of the keys is constant.

import {MultiKeyMap} from "lifecycle-utils";

type Provider = {name: string};
const provider1: Provider = {name: "1"};
const provider2: Provider = {name: "2"};

const map = new MultiKeyMap<[provider: Provider, name: string], number>();

map.set([provider1, "key1"], 1);
map.set([provider2, "key1"], 2);
map.set([provider1, "key2"], 3);

console.log(map.get([provider1, "key1"])); // 1
console.log(map.get([provider2, "key1"])); // 2
console.log(map.get([provider1, "key2"])); // 3

console.log([...map.keys()]); // [[{name: "1"}, "key1"], [{name: "2"}, "key1"], [{name: "1"}, "key2"]])

WeakValueMultiKeyMap

WeakValueMultiKeyMap is a utility class that works like a MultiKeyMap, but doesn't keep strong references to the values.

When a value is garbage collected, it is automatically removed from the map.

import {WeakValueMultiKeyMap} from "lifecycle-utils";

type Provider = {name: string};

const map = new WeakValueMultiKeyMap<[type: string, name: string], Provider>();

{
    const provider1: Provider = {name: "1"};
    map.set(["type1", "key1"], provider1);

    console.log(map.has(["type1", "key1"])); // true
    console.log(map.get(["type1", "key1"])); // {name: "1"}
    console.log(map.size); // 1
}

// wait for the runtime to run garbage collection
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 10));

console.log(map.has(["type1", "key1"])); // false
console.log(map.get(["type1", "key1"])); // undefined
console.log(map.size); // 0

WeakValueMap

WeakValueMap is a utility class that works like a Map, but doesn't keep strong references to the values.

When a value is garbage collected, it is automatically removed from the map.

import {WeakValueMap} from "lifecycle-utils";

type Provider = {name: string};

const map = new WeakValueMap<string, Provider>();

{
    const provider1: Provider = {name: "1"};
    map.set("provider1", provider1);

    console.log(map.has("provider1")); // true
    console.log(map.get("provider1")); // {name: "1"}
    console.log(map.size); // 1
}

// wait for the runtime to run garbage collection
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 10));

console.log(map.has("provider1")); // false
console.log(map.get("provider1")); // undefined
console.log(map.size); // 0

LongTimeout

A timeout that can be set to a delay longer than the maximum timeout delay supported by a regular setTimeout.

import {LongTimeout} from "lifecycle-utils";

const month = 1000 * 60 * 60 * 24 * 7 * 30;

const timeout = new LongTimeout(() => {
    console.log("timeout");
}, month);

// to clear the timeout, call dispose
// timeout.dispose();

setLongTimeout

Sets a timeout that can also be set to a delay longer than the maximum timeout delay supported by a regular setTimeout.

You can use clearLongTimeout to clear the timeout.

import {setLongTimeout, clearLongTimeout} from "lifecycle-utils";

const month = 1000 * 60 * 60 * 24 * 7 * 30;

const timeout = setLongTimeout(() => {
    console.log("timeout");
}, month);

// to clear the timeout, call clearLongTimeout
// clearLongTimeout(timeout);

clearLongTimeout

Clears a timeout that was set with setLongTimeout.

You can also clear a regular timeout with this function.

import {setLongTimeout, clearLongTimeout} from "lifecycle-utils";

const month = 1000 * 60 * 60 * 24 * 7 * 30;

const timeout = setLongTimeout(() => {
    console.log("timeout");
}, month);
const timeout2 = setTimeout(() => {
    console.log("timeout2");
}, 1000 * 60);

clearLongTimeout(timeout);
clearLongTimeout(timeout2);

State

State is a utility class that allows you to hold a value and notify listeners when the value changes.

import {State} from "lifecycle-utils";

const valueState = new State<number>(6);

const eventHandle = valueState.createChangeListener((newValue, previousValue) => {
    console.log("new value:", newValue);
    console.log("previous value:", previousValue);
});

valueState.state = 7;

// after a microtask, the listener will be called
// to make event fire immediately upon change, disable the `queueEvents` option on the constructor
await new Promise(resolve => setTimeout(resolve, 0));
// will print:
// new value: 7
// previous value: 6

eventHandle.dispose();

State.createCombinedChangeListener

Create a listener that listens to multiple states and calls the callback when any of the states change.

import {State} from "lifecycle-utils";

const valueState1 = new State<number>(6);
const valueState2 = new State<string>("hello");
const valueState3 = new State<boolean>(true);

const eventHandle = State.createCombinedChangeListener([valueState1, valueState2, valueState3], (newValues, previousValues) => {
    console.log("new values:", newValues);
    console.log("previous values:", previousValues);
});

valueState1.state = 7;
valueState2.state = "world";
valueState3.state = false;

// after a microtask, the listener will be called
// to make event fire immediately upon change, disable the `queueEvents` option on the constructor
await new Promise(resolve => setTimeout(resolve, 0));
// will print:
// new values: [7, "world", false]
// previous values: [6, "hello", true]

eventHandle.dispose();

splitText

Split a text by multiple separators, and return a result of the text and separators.

const parts = splitText("Hello <and> world [then] !", ["<and>", "[then]"]);
console.log(parts); // ["Hello ", new Separator("<and>"), " world ", new Separator("[then]"), " !"]

Queue

An efficient queue implementation that allows you to enqueue and dequeue items in O(1) time complexity.

import {Queue} from "lifecycle-utils";

const queue = new Queue([1, 2, 3]);

queue.push(4);
console.log(queue.shift()); // 1

console.log(queue.first); // 2
console.log(queue.last); // 4
console.log(queue.length); // 3
console.log([...queue]); // [2, 3, 4]

scopeExit

Create a scope exit handle that will call the provided callback when disposed, to be used with using or await using.

For example, this code:

import {scopeExit} from "lifecycle-utils";

function example() {
    using exitHandle = scopeExit(() => {
        console.log("exiting scope");
    });
    console.log("inside scope");
}

async function asyncExample() {
    await using exitHandle = scopeExit(async () => {
        await new Promise((resolve) => setTimeout(resolve, 100));
        console.log("exiting async scope");
    });
    console.log("inside async scope");
}

example();
console.log("example done");
console.log()

await asyncExample();
console.log("asyncExample done");

Will print this:

inside scope
exiting scope
example done

inside async scope
exiting async scope
asyncExample done

registerFinalizer

Register a finalizer for a given target, so that the finalizer is called after the target is garbage-collected.

A finalizer can be a function to call, an object to dispose, or a promise that resolves to one of the previous types.

import {DisposeAggregator, registerFinalizer} from "lifecycle-utils";

const disposeAggregator = new DisposeAggregator();
disposeAggregator.add(() => console.log("disposed"));

let obj: {} | null = {};
registerFinalizer(obj, disposeAggregator);

obj = null; // get rid of a reference to the object
await new Promise((accept) => setTimeout(accept, 1000 * 10)); // wait for the garbage collector

// disposed
import {registerFinalizer} from "lifecycle-utils";

let disposed1 = false;
let disposed2 = false;

let obj: {} | null = {};
const handle1 = registerFinalizer(obj, () => {
    disposed1 = true;
});
const handle2 = registerFinalizer(obj, () => {
    disposed2 = true;
});

console.log(disposed2.finalized); // false

handle1.dispose(); // remove the finalizer
obj = null; // get rid of a reference to the object

await new Promise((accept) => setTimeout(accept, 1000 * 10)); // wait for the garbage collector

console.log(disposed1); // false, because we removed the finalizer
console.log(disposed2); // true

console.log(disposed2.finalized); // true

sleep

Wait for a given duration. An optional AbortSignal can be provided to abort the wait.

import {sleep} from "lifecycle-utils";

await sleep(1000);

const controller = new AbortController();

const promise = sleep(1000, controller.signal);
controller.abort();

await promise; // rejected with controller.signal.reason

AbortablePromise

A Promise that can be rejected by an optional AbortSignal.

The executor can return a cleanup callback that will be called when the promise is resolved, rejected, or aborted.

import {AbortablePromise} from "lifecycle-utils";

const controller = new AbortController();

const promise = new AbortablePromise<number>(controller.signal, (resolve) => {
    const timeout = setTimeout(() => resolve(42), 1000);

    return () => {
        clearTimeout(timeout);
    };
});

controller.abort();

await promise; // rejected with controller.signal.reason

An existing Promise can be wrapped with an abort signal using AbortablePromise.withSignal:

const promise = AbortablePromise.withSignal(
    controller.signal,
    doSomething()
);

AbortablePromise also provides abortable versions of Promise.all, Promise.race, Promise.any, and Promise.allSettled:

const res = await AbortablePromise.all(controller.signal, [
    promise1,
    promise2
]);

withSingleFlight

Calling withSingleFlight runs its callback at most once in parallel for the given scope values, with parallel callers sharing the active callback's result. The result is only shared with other callers while the callback is running; later calls will run the callback again.

The callback holds an exclusive lock for the given scope while it runs. The callback accepts a signal argument that indicates when it should abort its execution. Aborting a caller makes its promise reject immediately, but only aborts the running callback's signal when no other callers are waiting for its result.

import {withSingleFlight, sleep} from "lifecycle-utils";

const scope = {};
let callCount = 0;

async function getValue() {
    return await withSingleFlight([scope, "myKey"], async () => {
        const value = ++callCount;
        await sleep(100);
        return value;
    });
}

const first = getValue();
await sleep(10);
const second = getValue();
await sleep(10);
const third = getValue();

console.log(await first); // 1
console.log(await second); // 1
console.log(await third); // 1
console.log(callCount); // 1

console.log(await getValue()); // 2
console.log(callCount); // 2
import {withSingleFlight, sleep} from "lifecycle-utils";

const scope = {};
let callCount = 0;
let aborted = false;
let done = false;

function getValue(signal?: AbortSignal) {
    return withSingleFlight([scope, "myKey"], signal, async (signal) => {
        signal.addEventListener("abort", () => {
            aborted = true;
        });

        const value = ++callCount;
        await sleep(100);

        done = true;
        return value;
    });
}

const firstController = new AbortController();
const secondController = new AbortController();

const first = getValue(firstController.signal);
await sleep(10);
const second = getValue(secondController.signal);

firstController.abort(new Error("Canceled"));

try {
    await first;
} catch (err) {
    console.log((err as Error).message); // "Canceled"
}

console.log(callCount); // 1
console.log(done); // false
console.log(aborted); // false

console.log(await second); // 1
console.log(callCount); // 1
console.log(done); // true
console.log(aborted); // false

Contributing

To contribute to lifecycle-utils see CONTRIBUTING.md.