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

@shware/utils

v2.0.3

Published

Readme

@shware/utils

Small, dependency-free helpers used across Shware apps and SDKs: string and array helpers, base62, invariant assertions, a promise memoizer, a retrying fetch, debounce and throttle, a token bucket, expiring localStorage, Stripe currency units, and a few React hooks. Everything is exported from the package root.

Install

pnpm add @shware/utils

react is an optional peer dependency, needed only for the React hooks.

Entry points

| Import | Contents | | --------------- | ---------------------------------------- | | @shware/utils | All helpers and React hooks listed below |

Strings, arrays and encoding

import { base62, chunk, hasText } from '@shware/utils';

hasText('  '); // false; also a type guard: str is string

chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]

const code = base62.encode(Buffer.from(uuid.replaceAll('-', ''), 'hex'));
const bytes = base62.decode(code, 16); // Buffer, left-padded to 16 bytes

base62 uses Node.js Buffer and preserves leading zero bytes.

Assertions

invariant(condition, message?) narrows condition and throws Error('Invariant failed: ...') when it is falsy. The message may be a function so it is only built on failure.

import { invariant } from '@shware/utils';

invariant(transaction.productId, 'Apple receipt productId is missing');
invariant(typeof price === 'number', () => `price is ${typeof price}`);

Promises

once(fn) memoizes an async function: concurrent calls share one in-flight promise, the first successful result is cached, and a rejection is not cached so the next call retries.

import { once } from '@shware/utils';

export const getCredentials = once(async () => {
  return JSON.parse(Buffer.from(env.CREDENTIALS_BASE64, 'base64').toString('utf-8'));
});

Fetch with retries

fetch(input, init) wraps the global fetch with exponential backoff (25% jitter) and honors Retry-After. Besides RequestInit it accepts RetryOptions:

| Option | Default | Description | | ---------------- | ------------------------------------------------------------ | ------------------------------------------- | | retries | 3 for GET, HEAD, PUT, DELETE, OPTIONS; 0 for POST, PATCH | Retries after the first attempt | | delayFactor | 500 | Base delay in ms, doubled on every retry | | maxDelay | 30_000 | Upper bound for a single delay in ms | | retryCondition | 408, 429, 500, 502, 503 or 504 status | (response) => boolean \| Promise<boolean> |

Network errors are retried too, under the same retries; after the last attempt the last response is returned (or the last error thrown). Only the idempotent methods are retried by default (RFC 9110 §9.2.2): a POST whose answer was lost may already have run on the server. Pass retries for a POST the server deduplicates — one carrying a client-generated id or an Idempotency-Key header, which is the caller's to choose, since it names the business operation being repeated. A request whose body is a ReadableStream is never retried.

import { fetch } from '@shware/utils';

const response = await fetch(pictureUrl, { retries: 3 });
if (!response.ok) throw new Error(`Picture request failed: ${response.status}`);

Debounce and throttle

import { debounce, throttle } from '@shware/utils';

const save = debounce((draft: string) => api.saveDraft(draft), 500, { maxWait: 2000 });
save('hello');
save.flush(); // run the pending call now
save.cancel(); // drop it

const onScroll = throttle(() => updateHeader(), 100, { leading: true, trailing: true });
window.addEventListener('scroll', onScroll);

DebounceOptions: leading (default false), trailing (default true), maxWait. ThrottleOptions: leading and trailing (both default true). Both return functions with cancel() and flush().

Token bucket

TokenBucket throttles outgoing work in-process. removeTokens() waits until requested tokens are available; rate tokens are added every interval (default 'second'). Call destroy() to clear its timer.

import { TokenBucket } from '@shware/utils';

const bucket = new TokenBucket({ rate: 10, capacity: 10, requested: 1 });

for (const user of users) {
  await bucket.removeTokens();
  await sendEmail(user);
}
bucket.destroy();

Browser helpers

expiringStorage stores JSON values in localStorage with a TTL in milliseconds; expired items read back as null and are removed.

import { expiringStorage } from '@shware/utils';

expiringStorage.setItem('pending-checkout', { priceId }, 30 * 60 * 1000);
const pending = expiringStorage.getItem<{ priceId: string }>('pending-checkout');
expiringStorage.removeItem('pending-checkout');

waitForRequestIdleCallback({ timeout?, signal? }) resolves when the browser is idle (falling back to a 1.5 s timer where requestIdleCallback is missing) and rejects if signal aborts:

import { waitForRequestIdleCallback } from '@shware/utils';

await waitForRequestIdleCallback({ timeout: 4000 });
loadAnalytics();

Stripe

stripeMinorUnits(currency) returns 1 for zero-decimal currencies (listed in ZERO_DECIMAL_CURRENCIES) and 100 otherwise.

import { stripeMinorUnits } from '@shware/utils';

const amount = 1999 / stripeMinorUnits('usd'); // 19.99
const yen = 1999 / stripeMinorUnits('jpy'); // 1999

React hooks

| Hook | Description | | ------------------------------------------ | -------------------------------------------------------------------------------- | | useDebouncedCallback(fn, delay, options) | Stable debounced callback, cancelled on unmount | | usePrevious(value) | Previous committed-render snapshot; initially undefined, updated by the effect | | useLatestRef(value) | Ref that always holds the latest value | | useIMEGuard() | Tracks IME composition so Enter does not submit mid-composition | | useExponentialHint(key, options) | Shows a hint again after exponentially growing dismiss intervals |

import { useDebouncedCallback, useExponentialHint, useIMEGuard } from '@shware/utils';

function Editor({ onSave }: { onSave: (text: string) => void }) {
  // flushOnExit: also flush when the tab is hidden or the component unmounts
  const save = useDebouncedCallback(onSave, 500, { flushOnExit: true });
  const { composing, onCompositionStart, onCompositionEnd } = useIMEGuard();

  return (
    <input
      onChange={(e) => save(e.target.value)}
      onCompositionStart={onCompositionStart}
      onCompositionEnd={onCompositionEnd}
      onKeyDown={(e) => {
        if (e.key === 'Enter' && !composing.current) save.flush();
      }}
    />
  );
}

function ShortcutHint() {
  // shown again 4 days after the first dismissal, 16 after the second, never after the third
  const { visible, dismiss } = useExponentialHint('shortcut-hint', {
    baseDays: 4,
    maxDismissals: 3,
  });
  return visible ? <Hint onClose={dismiss} /> : null;
}