usefulljs
v1.1.8
Published
A utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript
Maintainers
Readme
usefulljs
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 extendedArrayclass 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:
ArrayUFmethods (including native ones like.mapand.filter) return anArrayUFinstance, allowing for elegant and readable method chaining. - Lightweight & Zero-Dependency: Keeps your
node_modulesfolder clean and your bundle size small.
Installation
npm install usefulljs
# or
yarn add usefulljs
# or
bun add usefulljsAPI 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 bytoCanonicalString. 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 cachesretry<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):RetryOptionslimit: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: Returnstrueif the array has no items..isNotEmpty: Returnstrueif 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); //-> falseMethods
.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 newArrayUFwith all falsy values removed..duplicates([options]): Returns a newArrayUFcontaining duplicate elements, with configurable modes (all,first,subsequent) and an optionalaccessor..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 optionalaccessorfunction..shuffle(): Returns a newArrayUFwith the elements randomly shuffled..unique([options]): Returns a newArrayUFwith unique elements based on an optionalaccessorfunction.
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); // trueareEqual(...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")); // trueareNotEqual(...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)); // falsegetValue: 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. Usenullfor no expiration.kdf:"PBKDF2" | "HKDF" | "NONE"(default:"PBKDF2")"PBKDF2": best for passwords; CPU-hard. Configurepbkdf2Iterations(default100000)."HKDF": lightweight; use only with high-entropy secrets."NONE": treatsecretKeyas 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 whenkdfis"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 asencrypt.
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
CryptoErrorwith 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 ifkdf: "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 asencrypt.
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., anArrayBufferorUint8Array).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, andSetobjects (with elements in any order).Primitives and special values like
Date,RegExp,BigInt,Symbol, andundefined.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); // trueError 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.
- Fork the repository.
- Create your feature branch (
git checkout -b feature/AmazingFeature). - Commit your changes (
git commit -m 'Add some AmazingFeature'). - Push to the branch (
git push origin feature/AmazingFeature). - Open a pull request.
License
This project is licensed under the MIT License. See the LICENSE file for details.
