@pivanov/utils
v1.0.1
Published
A focused collection of TypeScript utilities for modern web development
Maintainers
Readme
@pivanov/utils
Features
- Fully typed - strict TypeScript across every module, literal-type preserving where it matters
- Tree-shakeable - ESM + CJS + per-module subpath exports,
"sideEffects": false - Zero dependencies - React is an optional peer dep only for the
useEventBushook - Well tested - 200+ tests, real edge cases (circular refs, typed arrays, Buffers, symbols)
Installation
bun add @pivanov/utilsnpm install @pivanov/utilsyarn add @pivanov/utilspnpm add @pivanov/utilsQuick start
import { camelCase, snakeCase, slugify } from '@pivanov/utils/string';
import { pick, groupBy, deepMerge } from '@pivanov/utils/object';
import { isString, isNil, isDefined } from '@pivanov/utils/assertion';
import { sleep, timeout, retry, parallelLimit } from '@pivanov/utils/promise';
import { deepClone, isEqual, busDispatch, useEventBus } from '@pivanov/utils/tools';What's inside
| Module | Surface |
|---|---|
| assertion | isString, isNumber, isBoolean, isFunction, isObject, isRecord, isNull, isUndefined, isNil, isDefined, isArray, isDate, isRegExp, isError, isPromise, isMap, isSet, isPrimitive, isEmpty |
| object | pick, omit, pickBy, omitBy, merge, deepMerge, mapValues, mapKeys, groupBy, invert, hasOwn, keysOf, entriesOf, fromEntries |
| promise | sleep, timeout, retry, defer, parallelLimit - all AbortSignal-aware where relevant |
| string | camelCase, pascalCase, kebabCase, snakeCase, titleCase, slugify, capitalize, uncapitalize, capitalizeFirstLetter, truncate, escapeHtml, escapeRegExp, words, lines |
| tools/deepClone | Rich deep clone (prototypes, getters/setters, symbols, Buffers, TypedArrays, circular refs) |
| tools/isEqual | Deep equality with cycle detection; compares RegExp, Error, TypedArrays, ArrayBuffer |
| tools/dom | isBrowser, checkVisibility, isInViewport, setStyleProperties, calculateRenderedTextWidth |
| cache | Cache Storage helpers: JSON metadata with optional TTL, plus raw Request/Response caching. Shipped as a standalone @pivanov/utils/cache entry point |
| tools/eventBus | busDispatch, busSubscribe, busOnce, useEventBus - typed topics, optional error handler |
| types | TDict, TObjType, DeepPartial, DeepReadonly, Mutable, Prettify |
See the full API documentation.
Tree shaking
Subpath imports give the smallest bundles:
import { camelCase } from '@pivanov/utils/string';
import { deepClone } from '@pivanov/utils/tools';Top-level imports tree-shake fine in modern bundlers:
import { camelCase, deepClone } from '@pivanov/utils';A few highlights
Async that cancels
import { sleep, timeout, retry } from '@pivanov/utils/promise';
const ctrl = new AbortController();
await sleep(1000, ctrl.signal); // cancellable
await timeout(fetch('/slow'), 3000); // race with timer
await retry(() => fetch('/api'), { attempts: 5, backoff: (n) => 100 * 2 ** n });Typed event bus
import { busDispatch, useEventBus, type IEventBus } from '@pivanov/utils/tools';
interface UserLoggedIn extends IEventBus<{ id: number; name: string }> {
topic: 'user:logged-in';
}
useEventBus<UserLoggedIn>('user:logged-in', (user) => console.log(user.name));
busDispatch<UserLoggedIn>('user:logged-in', { id: 1, name: 'Ada' });See the Typed Events guide for an event-map pattern at scale.
Cache with TTL
import {
storageSetItemWithTTL,
storageGetItemWithTTL,
} from '@pivanov/utils/cache';
await storageSetItemWithTTL('app', 'token', 'abc123', 10 * 60 * 1000);
const token = await storageGetItemWithTTL<string>('app', 'token');
// null if missing or expired; expired entries are deleted on readRaw response caching in a service worker
@pivanov/utils/cache is built as its own bundle with no React, no event bus and
no DOM helpers, so a service worker can import it without dragging the rest of
the package in. Alongside the JSON helpers it exposes a raw layer that stores a
Response byte for byte, preserving body, status and headers.
import {
cacheDelete,
cacheMatchResponse,
cacheNames,
cachePutResponse,
} from '@pivanov/utils/cache';
const CACHE = 'assets-v3';
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith((async () => {
const hit = await cacheMatchResponse(CACHE, event.request);
if (hit) {
return hit;
}
const response = await fetch(event.request);
await cachePutResponse(CACHE, event.request, response); // caches a clone
return response; // still readable
})());
});
// Revision cleanup
const outdated = (await cacheNames()).filter((name) => name !== CACHE);
await Promise.all(outdated.map(cacheDelete));Deep clone that actually preserves shape
import { deepClone } from '@pivanov/utils/tools';
class User {
constructor(public name: string) {}
greet() { return `hi ${this.name}`; }
}
const clone = deepClone(new User('Ada'));
clone instanceof User; // true
clone.greet(); // 'hi Ada'Compatibility
- Modern browsers (ES2022)
- Bun, Node 18+ (ESM or CJS)
- Cache API requires browser support (Chrome 40+, Firefox 41+, Safari 11.1+)
- React hook requires React 18+
Development
bun install
bun test # run tests
bun run test:coverage # with coverage (lcov)
bun run typecheck
bun run lint
bun run build # ESM + CJS + .d.ts
bun run docs:dev # run VitePress docs site locallySponsors
Supported by LogicStar AI
License
MIT © Pavel Ivanov
