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

@olvnck/helper

v2.0.2

Published

A simple helper library with common stuff

Readme

@olvnck/helper

A simple TypeScript helper library with common stuff: array/object/string/number/time utilities, an id-based promise resolver, a prioritized task queue, coalescing async queues, an event bus, a color class, plus browser-only download helpers and Vue composables.

  • ESM-only — the package ships .mjs + .d.mts only; there is no CommonJS build (require() will not work). This is the breaking change behind the 2.x major.
  • Modern baseline — Node.js ≥ 24 (enforced via engines) and evergreen browsers. The library deliberately contains no helpers that ES2024/ES2025 already provides natively (Object.fromEntries, Array.prototype.toSorted, Set.prototype.difference, Number.isInteger, …).
  • Fully typed — declarations are bundled, no @types package needed.
  • Three entry points — core helpers, browser-only helpers, and Vue composables are separate bundles so you only import what your environment supports.

| Entry point | Import path | Environment | | --- | --- | --- | | Core | @olvnck/helper | Node.js ≥ 24, Bun, browsers, workers | | Browser | @olvnck/helper/browser | Browsers only (needs document/window) | | Vue | @olvnck/helper/vue | Vue 3 apps (requires the vue peer dependency) |

Installation

npm install @olvnck/helper
# or
bun add @olvnck/helper

If you want to use the Vue composables, your app must have vue >= 3 installed — it is an optional peer dependency and is never bundled into this package.

Usage from Node.js

Everything in the core entry is environment-agnostic and works in Node.js, Bun, and browsers. Import named helpers directly from the package root:

import { addDays, dateToString, formatBytes, sleep } from '@olvnck/helper';

// time helpers — add*/sub* return a NEW Date, the input is never mutated
console.log(dateToString(addDays(new Date(), 7))); // "2026-06-19 14:05:33"
console.log(formatBytes(1536));                    // "1.50 KiB"
await sleep(250);

Formatting notes: formatBytes uses binary (1024-based) units with KiB/MiB-style labels; formatBitsPerSecond takes bytes as input and outputs decimal (1000-based) bit-rate units (kbit/s, Mbit/s, …) per networking convention. ltrim/rtrim truncate long strings with a ... ellipsis — they are not whitespace trimmers (use native trimStart/trimEnd for that).

Error conventions

All async helpers reject with Error objects (never bare strings). The messages to match on: 'timeout' (resolver timeout), 'canceled' (resolver clear()), 'limit reached' (runLimited over limit), 'cancelled' (task queue clear()), 'queue limit reached' (task queue per-id limit).

Resolver — resolve promises by id

useCustomResolver creates promises you can settle later from somewhere else, matched by an auto-generated id. Typical use: request/response over a WebSocket, where the reply arrives in a different handler than the request was sent from.

import { useCustomResolver } from '@olvnck/helper';

const resolver = useCustomResolver();

// somewhere: send a request, keep the promise
const reply = resolver.run(
    (id) => {
        socket.send(JSON.stringify({ id, type: 'getStatus' }));
    },
    { timeout: 5000 }, // optional: rejects with Error('timeout') after 5s
);

// elsewhere: the response handler settles it by id
socket.on('message', (msg) => {
    const { id, data } = JSON.parse(msg);
    resolver.resolve(id, data);
});

const status = await reply;

Ids default to an incrementing number; pass your own generator to control them: useCustomResolver<string>((prev) => crypto.randomUUID()). Other methods: runLimited(fn, type, maxLimit, opts) rejects immediately when too many promises of the same type are already pending, reject(id, err) settles with an error, has(id) / count() inspect pending promises, and clear() cancels everything (rejecting with Error('canceled') unless you pass false) and resets all runLimited counters.

Task queue — run async tasks one at a time, by priority

A lower prio value means higher priority. The queue starts processing immediately by default — add() kicks off the first task synchronously, so priority only reorders tasks that are still waiting. To batch tasks up front and let priority decide the full order, create the queue paused:

import { useTaskQueue } from '@olvnck/helper';

const queue = useTaskQueue<string>(false); // false = start paused

const a = queue.add({ id: 'thumbnail', prio: 2, fn: () => renderThumbnail() });
const b = queue.add({ id: 'export', prio: 1, fn: () => exportVideo() });

queue.resume();
await Promise.all([a, b]); // tasks ran sequentially, 'export' first

queue.pause(); // stop processing (resume() to continue)
queue.clear(); // rejects everything still queued with Error('cancelled')

add(task, limit) also accepts a per-id limit: if limit tasks with the same id are already queued, the returned promise rejects with Error('queue limit reached'). A task whose fn rejects passes its error through to the caller — the queue itself keeps running. Introspection: has() (anything queued), length(), print() (debug dump).

Coalescing queue — run the latest value, skip the rest

Where useTaskQueue runs every task you add, a coalescing queue runs one async operation at a time and keeps only the most recent value enqueued while that operation is in flight — intermediate values are discarded. It is the right tool for "persist the newest state", "re-fetch with the latest params", or "re-render from the freshest input", where running on a stale intermediate value would be wasted work.

import { CoalescingQueue } from '@olvnck/helper';

// only the most recent document state is ever saved; states that arrive
// while a save is in flight collapse into a single follow-up save
const saver = new CoalescingQueue<DocState>(async (state) => {
    await api.save(state);
});

editor.on('change', (state) => saver.enqueue(state)); // fire-and-forget

CoalescingKeyedQueue<K, V> is the same idea but with an independent in-flight slot per key — latest-wins within each key, and the key's internal state is dropped once it goes idle, so it is safe to use with an unbounded/growing key set:

import { CoalescingKeyedQueue } from '@olvnck/helper';

// one in-flight save per document id; the newest state for each id wins
const saver = new CoalescingKeyedQueue<string, DocState>(async (id, state) => {
    await api.save(id, state);
});

saver.enqueue(docId, state);

enqueue returns nothing and never rejects — the runner is expected to own its errors. A runner that rejects surfaces as an unhandled rejection rather than failing a specific caller, so wrap fallible work in try/catch inside the runner.

Event bus — decoupled pub/sub by channel id

import { useEventBus } from '@olvnck/helper';

type AppEvent = 'login' | 'logout';

const bus = useEventBus<AppEvent, { user: string }>('app');

const off = bus.on((event, data) => console.log(event, data.user));
bus.emit('login', { user: 'olivier' });
off();       // unsubscribe this listener
bus.reset(); // drop ALL listeners on this bus

Buses are singletons per id — calling useEventBus('app') anywhere in the process returns the same bus.

Event handler — collect cleanup functions

useEventHandler is a registry of teardown callbacks (socket closers, unsubscribers, …). Note that remove(fn) and clear() invoke each function before dropping it — it is a disposer, not a passive list:

import { useEventHandler } from '@olvnck/helper';

const cleanup = useEventHandler(); // or useEventHandler('shared-id') for a per-id singleton

cleanup.add(bus.on(onEvent), () => socket.close());

cleanup.clear(); // runs every registered fn, then empties the registry

Usage in the browser

The core entry (everything above) works in the browser as-is. The /browser entry adds helpers that require the DOM, mainly for triggering file downloads from in-memory data:

import { downloadAsBinary, downloadAsJson, downloadAsText, downloadVideoSnapshot } from '@olvnck/helper/browser';

// offer an object as a .json file download
downloadAsJson('report.json', { generated: Date.now(), items });

// plain text and binary variants
downloadAsText('notes.txt', 'hello');
downloadAsBinary('data.bin', [byteArray]);

// grab a frame from a <video> element as an image blob URL,
// optionally downloading it as a file at the same time.
// resolves to null if the frame could not be captured (canvas.toBlob returned null).
const blobUrl = await downloadVideoSnapshot(videoEl, { width: 1280, height: 720 }, { type: 'image/jpeg', quality: 0.8, downloadTo: 'snapshot.jpg' });
if (blobUrl) URL.revokeObjectURL(blobUrl); // the returned URL is yours to revoke when done

Import these only in code that runs in a browser — they touch document and window at call time, so using them under Node.js throws.

| Export | Purpose | | --- | --- | | downloadAsJson(file, obj) | download any object as pretty-printed JSON | | downloadAsText(file, str) | download a string as a text file | | downloadAsBinary(file, parts) | download BlobPart[] as a binary file | | downloadVideoSnapshot(video, res, opts?) | canvas-grab a video frame; resolves to an object URL, or null if capture fails (caller revokes) | | Resolution, SnapShotOpts | option types for the snapshot helper |

Usage with Vue

The /vue entry contains Vue 3 composables. vue is a peer dependency: it is resolved from your app and never bundled, so there is no risk of duplicate Vue instances.

delayedRef — a ref that applies certain values only after a delay

Each entry in the config maps a value to the delay (in ms) it must "survive" before the ref actually changes. The classic use case is a loading spinner that should only appear if loading takes noticeably long, but must disappear immediately:

<script setup lang="ts">
import { delayedRef } from '@olvnck/helper/vue';

// `true` is applied after 300ms, `false` immediately.
// If loading finishes within 300ms, the spinner never flashes.
const showSpinner = delayedRef(false, [
    { v: true, delay: 300 },
    { v: false, delay: 0 },
]);

async function load() {
    showSpinner.value = true;
    try {
        await fetchData();
    } finally {
        showSpinner.value = false;
    }
}
</script>

<template>
    <Spinner v-if="showSpinner" />
</template>

Setting a value that is already pending is a no-op, and setting a new value cancels the previous pending one. The options argument supports { ignoreInitial: true } to apply the very first write immediately regardless of its configured delay. Exported types: DelayedRef<T> (config entry), DelayedRefOptions.

useEventHandlerVue — scope-aware cleanup registry

The same disposer registry as the core useEventHandler ({ add, remove, clear }, returning EventHandlerReturn), but it runs clear() automatically when the current Vue effect scope is disposed — so you never write onUnmounted(() => cleanup.clear()):

<script setup lang="ts">
import { useEventHandlerVue } from '@olvnck/helper/vue';

const cleanup = useEventHandlerVue();

cleanup.add(bus.on(onEvent), () => socket.close());
// every registered fn runs on unmount — no onUnmounted boilerplate
</script>

It wraps an anonymous (non-shared) handler and registers teardown via Vue's native onScopeDispose, so it adds no VueUse dependency. Called outside an active effect scope it is a no-op registry — you would call clear() yourself, exactly like the core helper.

API overview (core entry)

| Module | Exports | | --- | --- | | Array | getId, arrayToMap (composite-key Map builder) | | Object | compareObj, compareObjFn (array diffing by unique keys → added/updated/unchanged/removed) | | String | formatBytes, formatBitsPerSecond, randomStr (crypto-based), ltrim, rtrim (ellipsis truncation) | | Number | gcd, getReadableAspect, minMax (clamp) | | Primitives | isNumeric | | Time | convertDateType, parseDate, parseDateToString, dateToString (+ dateToStringFile/dateToStringWithMs/dateToStringWithMsFile), addDays/subDays (+ months/years/hours/minutes/seconds), getDayOfYear, getDaysDiff (to − from), durationToString, formatDuration (colon-separated dd:hh:mm:ss, configurable depth/units/ms), sleep | | Functions | useTimeoutFn, customDebounceFn (promise-returning debounce), runOnChanged | | Async | useCustomResolver, useTaskQueue, CoalescingQueue, CoalescingKeyedQueue | | Events | useEventBus, useEventHandler | | Enums | enumHelper (numeric enums, including flag/pow2 enums) | | Colors | Colors (named-color lookup, hex/rgb/hsl/hwb conversions, lighten/darken/mix/rotate/…) | | Misc | createSingleton, utility types (MyFn, MyAwaitableFn, MyMutable, MyNullable, MyKeyValuePair) |

Number/date inputs accepted by all time helpers (DateType): a Date, a parseable string, or a unix timestamp (seconds or milliseconds — values below 10^10 are treated as seconds).

Exported types

Beyond the runtime helpers, the core entry also exports these supporting types so you can annotate variables that hold helper options or results:

| Type | Used with | Shape | | --- | --- | --- | | DateType | every time helper (input) | Date \| string \| number | | FormatTime | parseDate (return) | numeric { year, month, day, hour, min, sec, ms, dow } | | FormatTimeString | parseDateToString (return) | same fields zero-padded to strings (dow stays numeric) | | FormatDuration | formatDuration (options, pass as Partial) | { ms: boolean, digits: number, units: boolean }digits 1–5 picks how many units to show, units appends s/min/h/d, ms appends .mmm | | ResolverOptions<T> | useCustomResolver run/runLimited | { timeout, rejectOnTimeout?, onTimeout? } | | IdGenerator<T> | resolver custom id generator | (prev: T \| null) => T | | Task<T> | queue.add (input) | { id: string, prio: number, fn: () => T \| Promise<T> } | | UseTaskQueue | useTaskQueue (return) | the queue handle (add, pause, resume, clear, has, length, print) | | EventBusReturn<T, K> | useEventBus (return) | { on, emit, reset } | | EventHandlerReturn | useEventHandler (return) | { add, remove, clear } | | EnumKeyVal | enumHelper entries | { idx, v, isCombi, isPow2 } | | EnumHelperType | enumHelper (return) | { keyValues, toPrint, toArray, toNumber } | | TimeOutFnOptions | useTimeoutFn options | { immediate? } | | CustomDebounceFnArgs | customDebounceFn input fn | (...args) => Return | | SingletonFactory<T> | createSingleton (return) | (...args) => T |

The general-purpose utility types MyFn, MyAwaitableFn, MyMutable, MyNullable, and MyKeyValuePair (see the Misc row above) are exported too.

Development

Bun is the package manager and script runner.

bun install        # install dependencies
bun run build      # bundle all three entries to ESM + .d.mts in dist/ (vp pack → tsdown)
bun run dev        # same build in watch mode
bun run typecheck  # tsc --noEmit
bun run check      # Biome format + lint + import organization (add --write to fix)
bun run test       # manual smoke script (test/src/index.ts), not an assertion suite
bun run pub:dry    # preview the npm tarball
bun run pub        # publish to npmjs.org

Source lives in lib/: shared helpers in lib/lib/, browser-only code in lib/browser/, Vue composables in lib/vue/. To add a helper, create lib/lib/<name>.ts and re-export it from the matching entry file (lib/index.ts, lib/browser.ts, or lib/vue.ts).