@resolvx/core
v2.4.2
Published
Composable TypeScript primitives for production Node.js — Result types, async utilities, caches, structured logging, typed HTTP.
Downloads
727
Maintainers
Readme
@resolvx/core
Composable TypeScript primitives for production Node.js.
No magic. No framework opinions. Seven focused modules you can import individually or together. Tree-shakeable, typed end-to-end, zero mandatory dependencies.
npm install @resolvx/corechalk is an optional peer dependency for coloured log output. Install it if you want colour, skip it if you don't — the logger works either way.
Modules
result — Explicit error handling
Stop writing try/catch everywhere. Result<T, E> makes failure visible in function signatures.
import { capture, captureAsync, ok, err, combine, unwrapOr } from "@resolvx/core/result";
// Wrap a throwing function
const parsed = capture(() => JSON.parse(raw));
if (!parsed.ok) {
console.error("Bad JSON:", parsed.error);
return;
}
console.log(parsed.value);
// Async version
const result = await captureAsync(() => fetch(url).then(r => r.json()));
// Combine an array of Results — returns first Err or Ok([...values])
const all = combine([parseA, parseB, parseC]);
// Default on failure
const name = unwrapOr(parseName(input), "anonymous");API: ok err capture captureAsync map mapErr andThen tap tapErr unwrapOr unwrapOrElse unwrap combine partition fromNullable fromThrowable
async — Async control flow
import { retry, pool, memoize, debounce, throttle, once, withTimeout, sleep } from "@resolvx/core/async";
// Retry with exponential backoff + jitter
const data = await retry(() => fetchFromApi(url), {
attempts: 4,
baseDelayMs: 300,
isNonRetryable: e => e instanceof HttpError && e.status === 404,
onFailedAttempt: (err, attempt, nextDelay) =>
console.warn(`Attempt ${attempt} failed, retrying in ${nextDelay}ms`),
});
// Bounded concurrency — 5 requests at a time, output order preserved
const pages = await pool(urls.map(url => () => fetch(url).then(r => r.json())), 5);
// Memoize with TTL + in-flight deduplication
// Two concurrent calls with the same args share one execution
const getUser = memoize(fetchUser, { ttlMs: 60_000 });
// Timeout
const result = await withTimeout(() => heavyComputation(), 5000, "heavyComputation");API: sleep withTimeout retry pool memoize debounce throttle once
cache — In-memory caching
Three implementations, consistent interface.
import { TtlCache, LruCache, DedupeCache } from "@resolvx/core/cache";
// Time-to-live cache — entries expire after ttlMs
const prices = new TtlCache<string, number>(30_000);
const price = await prices.getOrSet("ETH", () => fetchPrice("ETH"));
// LRU cache — evicts least-recently-used when full
const images = new LruCache<string, Buffer>(100);
images.set("logo", buffer);
// Deduplication cache — concurrent callers share one in-flight request
const users = new DedupeCache<string, User>(60_000);
// Called 10 times simultaneously → only 1 fetchUser() call
const user = await users.getOrFetch(userId, id => fetchUser(id));API: TtlCache · LruCache · DedupeCache
fmt — Formatting
Pure functions. No side effects.
import { fmtNum, fmtUsd, fmtMs, fmtDuration, fmtBytes, fmtToken, fmtHex, fmtRelative, renderTable } from "@resolvx/core/fmt";
fmtNum(1_234_567.89, 2) // "1,234,567.89"
fmtUsd(1234.5) // "$1,234.50"
fmtMs(12_345) // "12.35s"
fmtDuration(3725) // "1h 2m 5s"
fmtBytes(1_536) // "1.50 KB"
fmtToken(1_500_000n, 6) // "1.500000" (bigint — no precision loss)
fmtHex("0xabcdef1234", 6, 4) // "0xabcd…3234"
fmtRelative(Date.now() / 1000 - 3700) // "1h 1m ago"
// No-dependency ASCII table
console.log(renderTable(
["Name", "Age"],
[["Alice", "30"], ["Bob", "25"]],
));log — Structured logger
Named namespaces, child loggers, pluggable transport.
import { createLogger } from "@resolvx/core/log";
const log = createLogger("api");
log.info("Server started", { port: 3000 });
log.warn("Rate limit approaching", { remaining: 5 });
log.error("Request failed", new Error("timeout"), { url: "/api/data" });
// Child logger — inherits level and transport, adds context to every line
const reqLog = log.child("request", { requestId: "abc-123" });
reqLog.info("Processing"); // → ns=request requestId=abc-123
// Measure async operations
const data = await log.time("fetchData", () => fetch(url).then(r => r.json()));
// Control level at runtime
log.setLevel("debug");
// Custom transport (e.g. JSON to stdout for log aggregators)
const log2 = createLogger("worker", {
transport: rec => process.stdout.write(JSON.stringify(rec) + "\n"),
});Set LOG_LEVEL env var to control the default level (debug | info | warn | error | silent).
net — Typed HTTP
import { fetchJson, getJson, postJson, RateLimiter, HttpError, NetworkError } from "@resolvx/core/net";
// GET with timeout + retry (3 attempts, backoff, 4xx not retried)
const user = await getJson<User>("https://api.example.com/users/1");
// POST JSON
const created = await postJson<Post>("/api/posts", { title: "Hello" });
// Error types
try {
await getJson("/api/data");
} catch (e) {
if (e instanceof HttpError) console.log(e.status, e.url);
if (e instanceof NetworkError) console.log("DNS/connection failure", e.cause);
}
// Rate limiter — 10 req/s, bursting to 20
const limiter = new RateLimiter({ rps: 10, burst: 20 });
const throttledFetch = limiter.wrap(() => getJson(url));
await Promise.all(Array.from({ length: 100 }, throttledFetch));types — TypeScript utilities
import { brand, assertUnreachable, hasKeys, isDefined, isString } from "@resolvx/core/types";
// Branded types — prevent mixing structurally identical primitives
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const uid = brand<UserId>("user-123");
// processOrder(uid) ↠type error if OrderId is expected
// Exhaustiveness check — compile error if a union case is unhandled
function describe(status: "ok" | "pending" | "failed"): string {
switch (status) {
case "ok": return "✓";
case "pending": return "…";
case "failed": return "✗";
default: return assertUnreachable(status);
}
}
// Runtime type narrowing
if (hasKeys(data, ["price", "symbol"])) {
console.log(data.price); // data is Record<"price"|"symbol", unknown>
}Requirements
- Node.js ≥ 18 (native
fetch) - TypeScript ≥ 5.0
Contributing
Bug reports and PRs welcome. Run npm test before submitting.
License
MIT
