@zero.sc/util
v0.0.4
Published
The small functions every service rewrites — measured, not guessed.
Maintainers
Readme
The problem this solves
Nobody needs another utility library. What this is actually about is that the same twenty lines get retyped in every codebase, and the retypings disagree with each other.
That claim is measured, not assumed. Across the services in this ecosystem: thirty-two had their
own clamp, fifteen had truncate, twelve had uuid, eleven had sleep, seven had slugify,
seven had retry. Then the copies were compared. One formatBytes preserved the sign of a
negative; the rest folded it to "0 B", so every deletion delta rendered as zero. Half the uuid
implementations were built from Math.random(). Most slugify implementations stripped with
[^a-z0-9], which erases a Korean title completely and leaves an empty slug — and empty slugs
collide.
None of those are hard problems. They are just problems that get solved slightly wrong, in private, over and over.
Install
npm install @zero.sc/utilNo dependencies. Everything is a pure function or a plain closure, so a bundler keeps only what you import.
Sixty seconds
import { clamp, formatBytes, retry, slugify, sleep, truncate } from '@zero.sc/util';
clamp(5, 10, 0); // 5 — inverted bounds are still a range
formatBytes(-2048); // '-2.0 KiB'
slugify('Café Crème'); // 'cafe-creme' — and a Hangul title survives intact
truncate('👨👩👧👦 family', 3); // no broken glyph at the cut
await retry(() => fetch(url).then((r) => r.json()), { attempts: 3 });
await sleep(200, signal); // cancellableWhat's inside
| Group | Exports |
|-------|---------|
| Numbers | clamp · normalize · lerp · round |
| Format | formatBytes · formatDuration · formatNumber |
| Text | slugify · truncate · graphemeLength · collapseWhitespace |
| Time | sleep · timeAgo · toIso |
| Functions | debounce · throttle · once |
| Retry | retry · backoff · NonRetryable |
| Identifiers | uuid · shortId |
What the copies got wrong
Each of these is a test in the suite, not a claim in a paragraph.
clampwith inverted bounds.Math.min(Math.max(v, min), max)returnsminwhen the bounds are swapped, silently. And NaN compares false against everything, so it passes straight through to the screen.roundat the floating-point boundary.Math.round(1.005 * 100) / 100is1, not1.01.formatBytesand negatives. Also the unit: dividing by 1024 and labelling itKBis the wrong label — that isKiB. Both bases are available; they are never mixed.slugifyand non-Latin titles. Latin diacritics come off, ideographs and Hangul stay. Decomposing to NFD and forgetting to recompose splits Hangul into six jamo where there were two syllables — a string that looks similar and is not the same.truncateand grapheme clusters.slicesplits a surrogate pair and leaves�; it also breaks the joins in family emoji into a different picture.sleepwithout cancellation. An aborted request leaves the timer alive to wake a path that was already abandoned. Inside a retry loop that leak accumulates.debouncewithoutcancel. A timer that fires after unmount calls setState on a component that is gone — and the usual next move is to reinvent anisMountedflag instead.throttledropping the last event. The position where scrolling stopped never lands, so the view freezes one frame behind.retrywithout jitter. Every worker that failed together returns together; the retry becomes the second outage. And retrying a 400 three times cannot change the answer.uuidfromMath.random(). As an idempotency key or session identifier, predictability is the vulnerability.
Honest limits
retryis for idempotent calls. Retrying a payment or a send creates duplicates. The library cannot know which yours is, and it does not guess.formatNumberandtimeAgofollow the runtime locale when you do not name one. A server and a browser can then disagree. Passlocalewhere that matters.truncateneedsIntl.Segmenterfor full grapheme correctness. Without it the fallback counts code points — surrogate pairs stay whole, joined emoji do not.- No collection helpers. No
groupBy, nochunk, no deep clone. Those were not among the things being retyped, and adding them would make this the library nobody needs. - This is 0.0.x. The names are the part most likely to move.
Around it
| | |
|---|---|
| Docs | kit.zero.sc |
| Events, which uses this for backoff | @zero.sc/events |
| Calling services | @zero.sc/sdk |
| Components | @zero.sc/ui |
| Everything | @zero.sc |
License
MIT OR Zero License v1.0 — take whichever you prefer. Choosing MIT is enough; nothing further is required of you.
The Zero name, marks and logos are not covered — build anything you like with this code, just don't present it as a Zero product.
Copyright (c) 2026 Zero. Source Code begins at Zero.
