Skip to content

Choosing a utility

The API reference lists every export. This page answers the question it cannot: which of two similar things do I want, and why.

Use when you know roughly what you need — "run this less often", "only once", "store this", "talk to a worker" — but not which export does it.

First: does the platform already have it?

ranuts is not trying to replace the standard library. Reach for the platform first, and use a utility when it genuinely adds something:

Instead of…The platform has…Use the ranuts one when…
cloneDeep(value)structuredClone(value)The value contains functions or anything structuredClone refuses — it throws a DataCloneError, cloneDeep copies what it can and keeps the rest by reference.
getAllQueryString(url)new URL(url).searchParamsYou want a plain object in one call rather than an iterator.
localStorageGetItem(key)localStorage.getItem(key)The code also runs where storage is missing or blocked — the wrappers return '' instead of throwing (Safari private mode, SSR, a sandboxed iframe).
escapeHtml(str)textContent = strYou are building a string, not a node.

Doing something less often

Four different questions hide behind "call this less":

You want…UseBehaviour
Only the last call in a burst — a search box, a resizedebounce(fn, ms)Runs ms after the burst stops. Nothing runs during the burst.
A steady rate during a burst — scroll position, a progress readoutthrottle(fn, ms)The first call runs immediately, then at most one per ms.
It to run exactly once, ever — an init, a one-time warningonce(fn)First call evaluates, every later call returns that same result.
Concurrent callers to share one in-flight requestsingleFlight(fn)The async flavour of once: while a call is pending, others join it.

memoize is the former name of once and does the same thing — it does not cache per argument, which is what the name suggests. New code should say once.

The difference that bites: debounce on a keystroke handler means nothing runs while the user is typing; throttle means something runs the whole time, just not on every key. A search suggestion wants debounce; a "characters remaining" counter wants throttle.

Running async work under control

You want…Use
To run many tasks, but only n at a timenew QuestQueue({ simultaneous: n })
To give up on a promise that takes too longwithTimeout(promise, ms)
…and carry on with a default instead of throwingwithTimeoutFallback(promise, ms, fallback)
A promise you resolve from somewhere else entirelydeferred()
To chain async steps, Koa-style, each able to wrap the nextcompose(middleware)

Promise.all is right when you want all of them at once; QuestQueue is right when "all at once" would open sixty connections. withTimeout rejects — pair it with a catch, or use the fallback variant when a timeout is not an error for you.

Storing something

Lifetime and sizeUse
A small string that survives reloadslocalStorageSetItem / localStorageGetItem / localStorageRemoveItem
Structured data, many records, or more than a few MBnew WebDB({ dbName, stores }) — a promise wrapper over IndexedDB
One value handed from this page to the nextcreateHandoff({ dbName, storeName, key })

The localStorage* wrappers exist because the native calls throw where storage is unavailable — Safari's private mode, a sandboxed iframe, a browser with site data blocked — and a crash on read is a worse failure than a missing preference. They return '' and carry on.

createHandoff is for the case neither of the others fits: a value that must survive exactly one navigation and then be gone.

Talking across contexts

Between…Use
A page and a Web Worker, request/responsenew WorkerClient({ create }) — correlates replies by request id
Any two MessagePort endscreatePortBridge(port)
Two windows/iframes that must find each otheracceptPortBridge() on one side, the handshake on the other

WorkerClient is the one to reach for when the worker answers questions: without request ids, two overlapping calls cannot tell whose reply arrived. A bridge is the lower level — use it when the traffic is not request/response, or when the transport already exists.

Working with objects

You want…UseNote
A copy nothing else sharescloneDeep(value)Handles circular references and the common built-ins.
To know whether two values are alikeisEqual(a, b)Deep comparison, not reference identity.
To combine two objectsmerge(a, b)
To drop some keysfilterObj(obj, keys)Returns a copy without the listed keys.

Locale and text

  • resolveLocale({ supported, … }) picks which of your locales to use, from the usual chain (an explicit choice, storage, navigator.languages, a fallback). It answers "which language", not "what does this string say".
  • createI18n / useI18n (ranuts/i18n) is the translation engine — flat message dictionaries, {param} interpolation, runtime switching.
  • segmentByRanges and paginateText are for laying text out: offsets and highlights, and cutting text into pages that fit a box.

Use resolveLocale even if you are not using the i18n engine — the decision it makes (respect the reader's ordered navigator.languages, not only the first) is the part that is easy to get wrong.

Streaming a model response

Three layers, each usable alone:

  1. ranuts/stream — parse SSE, then fold the deltas into a snapshot with createStreamAccumulator(). Provider-neutral: text, reasoning and tool-call deltas end up in the same shape regardless of who emitted them.
  2. ranuts/conversation — project an append-only event log into renderable nodes with createConversationEngine(). It decides what a row is; it draws nothing.
  3. <r-conversation> in ranui — the element that renders those nodes, keeps the view pinned to the bottom, and reconciles rows.

Stop at layer 1 if you are only rendering text; add 2 when a transcript has structure worth projecting; add 3 when you want the scrolling and reconciliation solved.

Which entry to import from

Every subpath is an independent, tree-shakeable barrel — import from the one that owns the symbol, never from a deep source path.

ImportContainsRuntime
ranutsRoot barrel — the utils + visual surfacebrowser + node
ranuts/utilsDOM/BOM, string, object, number, colour, time, storage, …browser + node*
ranuts/nodeHTTP server, router, WebSocket, fs, streams, middlewarenode only
ranuts/visualThe 2D rendering engine (Canvas / WebGL / WebGPU)browser only
ranuts/i18nThe translation engine, DOM-freebrowser + node
ranuts/swCache strategies and the precache protocol's worker halfservice worker
ranuts/vnodeSnabbdom-style virtual DOMbrowser
ranuts/streamSSE parsing, model-stream fold, token budgetbrowser + node
ranuts/conversationEvent log → renderable conversation nodesbrowser + node

* ranuts/utils is broad: most of it is browser-oriented, but the pure helpers (string, object, number, compose, cloneDeep, …) run anywhere. Do not import ranuts/node in browser code — it pulls in fs / http / child_process.

Still not sure?

Search the API reference — every export is there with its signature and a one-line description, generated from source. If two of them still look interchangeable after reading both lines, that is a documentation bug worth reporting.

Released under the MIT License.