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 🙏

© 2025 – Pkg Stats / Ryan Hefner

usefulljs

v1.1.8

Published

A utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript

Readme

usefulljs

MIT License npm version

A lightweight, powerful, and zero-dependency utility library for TypeScript and JavaScript that simplifies complex asynchronous operations, enhances array manipulations, and provides robust cryptography tools.


Overview

usefulljs provides a set of straight-forward, robust, and efficient functions designed to tackle common challenges in modern web development. Whether you need to prevent redundant API calls, handle transient network errors gracefully, perform complex data analysis on arrays, or secure your data with modern cryptography, this library has you covered.

Built with TypeScript, it offers full type safety and is designed for seamless integration into any project, supporting both ES Modules and CommonJS.

Features

  • Robust Async Control:
    • singleExec: Guarantees that an async function is only executed once at a time for a given key, preventing race conditions and redundant operations.
    • retry: Automatically retries a failing async task with a configurable exponential backoff strategy, perfect for handling unreliable network requests.
  • Powerful Array Utilities:
    • ArrayUF: An extended Array class that supercharges your data manipulations with convenient getters and powerful methods.
  • Object Utilities:
    • areEqual, areNotEqual: Deeply compare any JavaScript values.
    • toCanonicalString: Create a deterministic string representation of any JavaScript value.
  • Secure Cryptography:
    • encrypt / decrypt: Robust AES-GCM encryption for raw binary data (Uint8Array).
    • encryptString / decryptString: The same robust encryption for strings, with URL-safe base64url output.
    • encryptStream / decryptStream: Streaming encryption and decryption for large payloads using Web Streams.
    • hashObject: Create a deterministic SHA-256 hash of any JavaScript value, including complex nested objects, Maps, Sets, and even structures with circular references.
  • Type-Safe: Fully written in TypeScript to provide excellent autocompletion and catch errors at compile time.
  • Seamless Chaining: ArrayUF methods (including native ones like .map and .filter) return an ArrayUF instance, allowing for elegant and readable method chaining.
  • Lightweight & Zero-Dependency: Keeps your node_modules folder clean and your bundle size small.

Installation

npm install usefulljs
# or
yarn add usefulljs
# or
bun add usefulljs

API Documentation

A quick look at the utilities this package provides. Click on any utility to see its details.


singleExec<TResult>(taskFn, [key])

Ensures that an asynchronous task is only executed once at a time for a given unique key. If called again with the same key while the task is running, it returns the promise of the existing task.

  • taskFn: () => Promise<TResult> — The asynchronous function to execute.
  • key (optional): SerializableKey — A unique identifier for the task. Can be a string, number, object, or array. If not provided, a key is generated by hashing the function's source code.
import { singleExec } from "usefulljs/singleExec";

async function fetchUser(userId: string) {
  return singleExec(
    () => {
      console.log(`Fetching user ${userId}...`);
      // Imagine this is an API call
      return new Promise((resolve) =>
        setTimeout(() => resolve({ id: userId, name: "John Doe" }), 100)
      );
    },
    `user-${userId}` // Unique key for this user
  );
}

// Both calls will trigger only one "Fetching user 123..." log
Promise.all([fetchUser("123"), fetchUser("123")]);

SingleExecution (class)

A SingleFlight-style executor that deduplicates concurrent async work by a key. Concurrent calls with the same key share a single in-flight Promise and resolve/reject together. Once settled, the entry is removed so future calls re-execute.

Constructor:

  • new SingleExecution(options?)
    • options.scope?: string — Optional namespace that becomes part of the key, letting you isolate keys across instances.

Methods:

  • run(taskFn, [key])Promise<TResult>
    • taskFn: () => Promise<TResult>
    • key?: SerializableKey — Any value representable by toCanonicalString. If omitted, taskFn.toString() is used.
  • size (getter) → number — Number of in-flight entries.
  • clear()void — Clears the in-flight map (useful for tests or shutdown).
import { SingleExecution } from "usefulljs/singleExec";

const single = new SingleExecution({ scope: "api" });

// Deduplicate concurrent fetches for the same user
function fetchUser(userId: string) {
  return single.run(
    () => Promise.resolve({ id: userId }), // e.g., api.get(`/users/${userId}`)
    `user:${userId}`
  );
}

await Promise.all([fetchUser("1"), fetchUser("1")]); // Executes once

// Object keys are canonicalized (order-agnostic)
await Promise.all([
  single.run(() => Promise.resolve("ok"), { a: 1, b: 2 }),
  single.run(() => Promise.resolve("ok"), { b: 2, a: 1 }),
]); // single execution

// Isolated instances: same key in different instances run independently
const singleA = new SingleExecution({ scope: "A" });
const singleB = new SingleExecution({ scope: "B" });
await Promise.all([
  singleA.run(() => Promise.resolve("A"), "key"),
  singleB.run(() => Promise.resolve("B"), "key"),
]);

singleExecutionService

The shared, application-level instance of SingleExecution used by singleExec. Use when you want to manage the shared instance (e.g., for clearing the the cache with .clear()).

import { singleExecutionService } from "usefulljs/singleExec";

// Deduplicate concurrent calls using the shared service
async function fetchConfig() {
  return singleExecutionService.run(
    () =>
      Promise.resolve({
        /* config */
      }),
    "config"
  );
}

await Promise.all([fetchConfig(), fetchConfig()]); // Executes once

singleExecutionService.clear(); // Clears the shared, application-level caches

retry<TResult>(taskFn, [options])

Executes an asynchronous task and automatically retries it with an exponential backoff strategy if it fails.

  • taskFn: () => Promise<TResult> — The asynchronous function to execute.
  • options (optional): RetryOptions
    • limit: number (default: 2) — The maximum number of retry attempts.
    • backoff (optional):
      • initialDelay: number (default: 0) — Initial delay in milliseconds before the first retry.
      • maxDelay: number (default: Infinity) — Maximum delay between retries.
    • onRetry: (error, attempt, delay) => void — Callback executed before each retry.
import { retry } from "usefulljs/retry";

let attempt = 0;
async function fetchUnreliableData() {
  attempt++;
  console.log(`Attempt #${attempt}...`);
  if (attempt < 3) {
    throw new Error("Network error");
  }
  return { data: "Finally!" };
}

const data = await retry(fetchUnreliableData, {
  limit: 3,
  backoff: {
    initialDelay: 100,
  },
  onRetry: (error, attempt) => {
    console.log(`Attempt ${attempt} failed. Retrying...`);
  },
});

console.log(data); // { data: 'Finally!' }

ArrayUF<T>

An extended Array class with convenient getters and powerful utility methods.

import { ArrayUF } from "usefulljs/array";

const numbers = new ArrayUF([1, 2, 3, 4, 5]);

Getters

  • .isEmpty: Returns true if the array has no items.
  • .isNotEmpty: Returns true if the array has one or more items.
  • .first: Returns the first element.
  • .last: Returns the last element.
  • .middle: Returns the middle item(s) of the array based on their index, not their value. This is not a median calculation.
  • .random: Returns a random element.
const fullList = new ArrayUF([10, 20, 30]);
console.log(fullList.isEmpty); //-> false
console.log(fullList.isNotEmpty); //-> true
console.log(fullList.first); // 10
console.log(fullList.last); // 30
console.log(fullList.middle); // 20

const emptyList = new ArrayUF();
console.log(emptyList.isEmpty); //-> true
console.log(emptyList.isNotEmpty); //-> false

Methods

  • .chunk(size): Splits the array into smaller arrays (chunks) of a specified size.
  • .clear(): Clears all elements from the array, making it empty.
  • .compact(): Returns a new ArrayUF with all falsy values removed.
  • .duplicates([options]): Returns a new ArrayUF containing duplicate elements, with configurable modes (all, first, subsequent) and an optional accessor.
  • .groupBy(accessor): Groups the elements of the array into an object based on a key generated by the accessor function.
  • .mostFrequent([accessor]): Finds the most frequently occurring item(s), using an optional accessor function.
  • .shuffle(): Returns a new ArrayUF with the elements randomly shuffled.
  • .unique([options]): Returns a new ArrayUF with unique elements based on an optional accessor function.
import { ArrayUF } from "usefulljs/array";

// clear()
const list = new ArrayUF([1, 2, 3]);
list.clear();
console.log(list.isEmpty); //-> true

// chunk()
const numbers = new ArrayUF([1, 2, 3, 4, 5]);
const chunks = numbers.chunk(2);
console.log(chunks); // ArrayUF[ArrayUF[1, 2], ArrayUF[3, 4], ArrayUF[5]]

// compact()
const mixed = new ArrayUF([0, 1, false, 2, "", 3, null]);
const compacted = mixed.compact();
console.log(compacted); // ArrayUF[1, 2, 3]

// groupBy()
const users = new ArrayUF([
  { name: "Alice", department: "HR" },
  { name: "Bob", department: "Engineering" },
  { name: "Charlie", department: "HR" },
]);
const grouped = users.groupBy((user) => user.department);
// grouped is:
// {
//   HR: ArrayUF[{ name: 'Alice', ... }, { name: 'Charlie', ... }],
//   Engineering: ArrayUF[{ name: 'Bob', ... }]
// }

// shuffle()
const shuffled = numbers.shuffle();
console.log(shuffled); // e.g., ArrayUF[3, 5, 1, 4, 2]

// unique()
const withDuplicates = new ArrayUF([1, 2, 2, 3, 1, 4]);
const uniqueItems = withDuplicates.unique();
console.log(uniqueItems); // ArrayUF[1, 2, 3, 4]

const usersWithDupes = new ArrayUF([
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 1, name: "Alicia" },
]);
const uniqueUsers = usersWithDupes.unique((user) => user.id);
console.log(uniqueUsers); // ArrayUF[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

// Method Chaining
const products = new ArrayUF([
  { category: "A", price: 10 },
  { category: "B", price: 20 },
  { category: "A", price: 30 },
  { category: "C", price: 20 },
]);

// Get the first product from the 'A' category after sorting by price
const result = products
  .filter((p) => p.category === "A")
  .sort((a, b) => a.price - b.price).first;

console.log(result); // { category: 'A', price: 10 }

Object Utilities

Provides functions for comparing and serializing JavaScript objects.

toCanonicalString(value)

Converts any JavaScript value into a stable, canonical string representation. This is the foundation for areEqual and is also useful for creating consistent hashes or keys from objects.

  • value: any — The value to convert.
import { toCanonicalString } from "usefulljs/object";

const obj1 = { b: 2, a: 1 };
const str1 = toCanonicalString(obj1); // '{"a":1,"b":2}'

const obj2 = { a: 1, b: 2 };
const str2 = toCanonicalString(obj2); // '{"a":1,"b":2}'

console.log(str1 === str2); // true

areEqual(...values)

Checks if all given values are deeply equal by comparing their canonical string representations.

  • ...values: any[] — The values to compare.
import { areEqual } from "usefulljs/object";

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { b: { c: 2 }, a: 1 };
const obj3 = { a: 1, b: { c: 3 } };

console.log(areEqual(obj1, obj2)); // true
console.log(areEqual(obj1, obj3)); // false
console.log(areEqual("test", "test", "test")); // true

areNotEqual(...values)

Checks if any of the given values are not deeply equal. It is the logical opposite of areEqual.

  • ...values: any[] — The values to compare.
import { areNotEqual } from "usefulljs/object";

const obj1 = { a: 1 };
const obj2 = { a: 2 };

console.log(areNotEqual(obj1, obj2)); // true
console.log(areNotEqual(obj1, obj1)); // false
  • getValue: Safely access nested properties of an object.

pick(obj, keys)

Creates a new object composed of the picked object properties.

  • obj: object — The source object.
  • keys: string[] — The properties to pick.
import { pick } from "usefulljs/object";

const obj = { a: 1, b: "hello", c: true };
const picked = pick(obj, ["a", "c"]);

console.log(picked); // { a: 1, c: true }

omit(obj, keys)

Creates a new object with properties from the source object that are not omitted.

  • obj: object — The source object.
  • keys: string[] — The properties to omit.
import { omit } from "usefulljs/object";

const obj = { a: 1, b: "hello", c: true };
const omitted = omit(obj, ["b"]);

console.log(omitted); // { a: 1, c: true }

Cryptography

Secure data handling built on the Web Crypto API, offering string, binary, and streaming encryption.

encrypt(data, secretKey, [options])

Encrypts raw binary data (BufferSource) using a robust, headered AES-GCM format.

  • data: BufferSource — The binary data to encrypt (e.g., Uint8Array, ArrayBuffer).
  • secretKey: string — The secret for key derivation.
  • options (optional):
    • ttl: number | null (default: 3600000) — Time-to-live in milliseconds. Use null for no expiration.
    • kdf: "PBKDF2" | "HKDF" | "NONE" (default: "PBKDF2")
      • "PBKDF2": best for passwords; CPU-hard. Configure pbkdf2Iterations (default 100000).
      • "HKDF": lightweight; use only with high-entropy secrets.
      • "NONE": treat secretKey as a raw AES key; its UTF-8 byte length must be exactly 16 (AES‑128) or 32 (AES‑256).
    • keyLengthBits: 128 | 256 (default: 256) — AES key size; 128 is often faster.
    • hash: "SHA-256" | "SHA-384" | "SHA-512" (default: "SHA-256") — Hash for PBKDF2/HKDF.
    • pbkdf2Iterations: number (default: 100000) — Used only when kdf is "PBKDF2".

Returns: Promise<Uint8Array> — The encrypted data as [header | salt | iv | ciphertext].

See also: encryptString for encrypting strings, or encryptStream for streaming encryption.

import { encrypt, decrypt } from "usefulljs/crypto";

const plaintext = new TextEncoder().encode("Hello, raw bytes!");
const secret = "a-very-secret-key";

const encrypted = await encrypt(plaintext, secret);
const decrypted = await decrypt(encrypted, secret);

console.log(new TextDecoder().decode(decrypted)); // "Hello, raw bytes!"

decrypt(encryptedBytes, secretKey)

Decrypts raw binary data from encrypt.

  • encryptedBytes: BufferSource — The encrypted byte array.
  • secretKey: string — The same secret used for encryption.

Returns: Promise<Uint8Array> — The original plaintext bytes.

See also: decryptString for decrypting base64url strings, or decryptStream for streaming decryption.

encryptString(plaintext, secretKey, [options])

Encrypts a UTF-8 string using AES-GCM and embeds a compact, authenticated header that carries all parameters required for decryption. The output is base64url-encoded (URL-safe, no padding).

  • plaintext: string — The string to encrypt.
  • secretKey: string — The secret used for key derivation or as a raw AES key.
  • options (optional): Same options as encrypt.

Returns: Promise<string> — A base64url token [header | salt | iv | ciphertext].

Security notes:

  • The header is provided to AES-GCM as Additional Authenticated Data (AAD); any tampering makes decryption fail.
  • TTL is enforced during decryption; when expired, you’ll get CryptoError with code "EXPIRED".
import { encryptString } from "usefulljs/crypto";

// Default: PBKDF2 + AES-256-GCM, 1-hour TTL
const token = await encryptString("Hello, World!", "my strong passphrase");

// No expiration
const permanent = await encryptString("Persistent", "secret", { ttl: null });

// Faster: HKDF + AES-128-GCM (use only with high-entropy secret)
const fast = await encryptString("Data", "random-long-secret", {
  kdf: "HKDF",
  keyLengthBits: 128,
  ttl: null,
});

// Raw key (kdf: NONE) with AES-256-GCM (secret must be 32 UTF-8 bytes)
const raw256 = await encryptString(
  "Sensitive",
  "0123456789abcdef0123456789abcdef",
  { kdf: "NONE", keyLengthBits: 256 }
);

decryptString(encryptedData, secretKey)

Decrypts a token created by encryptString. No options are required; algorithm parameters are read from the embedded header.

  • encryptedData: string — Base64url token.
  • secretKey: string — The same secret used for encryption (or the raw AES key if kdf: "NONE" was used).

Throws CryptoError on:

  • "UNSUPPORTED_ENVIRONMENT" — Web Crypto API is unavailable.
  • "INVALID_DATA" — Malformed or undecodable token.
  • "EXPIRED" — TTL has elapsed.
  • "DECRYPTION_FAILED" — Wrong key or tampered data (header/ciphertext/tag).
import { decryptString } from "usefulljs/crypto";

const decrypted = await decryptString(token, "my strong passphrase");
console.log(decrypted);

Streaming Encryption

For large files or data streams, you can use Web Streams to encrypt and decrypt without buffering the entire content in memory.

encryptStream(secretKey, [options])

Creates a TransformStream that encrypts chunks of Uint8Array data.

  • secretKey: string — The secret for key derivation.
  • options (optional): Same options as encrypt.

The output stream consists of a single prologue ([header | salt | baseIV]) followed by any number of frames ([sequence | ciphertext_length | ciphertext]).

import { encryptStream } from "usefulljs/crypto";

const response = await fetch("large-file.zip");
const secret = "your-secret-key";

const encryptedStream = response.body.pipeThrough(
  encryptStream(secret, { kdf: "HKDF" })
);

// You can now pipe this stream elsewhere, e.g., upload it
// await fetch('/upload', { method: 'POST', body: encryptedStream });

decryptStream(secretKey)

Creates a TransformStream that decrypts a stream produced by encryptStream.

  • secretKey: string — The same secret used for encryption.

It correctly parses the prologue and decrypts each frame in sequence, ensuring data integrity and authenticity.

import { decryptStream } from "usefulljs/crypto";

const response = await fetch("/encrypted-file");
const secret = "your-secret-key";

const decryptedStream = response.body.pipeThrough(decryptStream(secret));

// Consume the decrypted stream
for await (const chunk of decryptedStream) {
  console.log(chunk); // Uint8Array chunk of original data
}

Compatibility:

  • Backward compatible: tokens generated by older versions (legacy layout with PBKDF2/SHA‑256 + AES‑256‑GCM and non‑URL‑safe base64) are still accepted by decryptString.

hash(data, [algorithm])

Asynchronously calculates a hash of the given data using the specified algorithm. This is a direct interface to the Web Crypto API's digest method.

  • data: BufferSource — The data to hash (e.g., an ArrayBuffer or Uint8Array).
  • algorithm: AlgorithmIdentifier (optional, default: "SHA-256") — The hashing algorithm to use (e.g., "SHA-256", "SHA-384", "SHA-512").
import { hash } from "usefulljs/crypto";

const data = new TextEncoder().encode("hello world");

// Calculate SHA-256 hash (default)
const sha256Hash = await hash(data);
console.log(sha256Hash); // "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

// Calculate SHA-512 hash
const sha512Hash = await hash(data, "SHA-512");

hashObject(obj, [algorithm])

Calculates a deterministic hash of any JavaScript value using a specified algorithm.

This function creates a consistent, canonical string representation of any object before hashing. It correctly handles complex and nested data structures, including:

  • Objects with keys in any order.

  • Arrays, Map, and Set objects (with elements in any order).

  • Primitives and special values like Date, RegExp, BigInt, Symbol, and undefined.

  • Objects with circular references.

  • obj: any — The value to hash.

  • algorithm: AlgorithmIdentifier (optional, default: "SHA-256") — The hashing algorithm to use.

import { hashObject } from "usefulljs/crypto";

const obj1 = { b: { d: new Set([1, new Date(0)]), c: 3 }, a: 2 };
const obj2 = { a: 2, b: { c: 3, d: new Set([new Date(0), 1]) } };

// Default SHA-256 hash
const hash1 = await hashObject(obj1);
const hash2 = await hashObject(obj2);
console.log(hash1 === hash2); // true

// Using a different algorithm
const sha512Hash = await hashObject(obj1, "SHA-512");
console.log(hash1 !== sha512Hash); // true

Error Handling

The cryptography functions throw a CryptoError for specific, catchable failures.

  • CryptoError.code:
    • UNSUPPORTED_ENVIRONMENT: The Web Crypto API is not available.
    • ENCRYPTION_FAILED: Encryption process failed.
    • DECRYPTION_FAILED: Decryption failed (wrong key or tampered data).
    • INVALID_DATA: The encrypted payload is malformed or not base64url.
    • EXPIRED: The data’s TTL has passed.
import { decryptString, CryptoError } from "usefulljs/crypto";

try {
  const decrypted = await decryptString(expiredData, secret);
} catch (error) {
  if (error instanceof CryptoError) {
    switch (error.code) {
      case "EXPIRED":
        console.error("The data has expired!");
        break;
      case "DECRYPTION_FAILED":
        console.error("Wrong key or tampered data.");
        break;
      default:
        console.error("Crypto error:", error);
    }
  } else {
    console.error("Unexpected error:", error);
  }
}

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue.

  1. Fork the repository.
  2. Create your feature branch (git checkout -b feature/AmazingFeature).
  3. Commit your changes (git commit -m 'Add some AmazingFeature').
  4. Push to the branch (git push origin feature/AmazingFeature).
  5. Open a pull request.

License

This project is licensed under the MIT License. See the LICENSE file for details.