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

@sera4/essentia

v3.0.23

Published

A library of utilities for Teleporte Web Services

Readme

ESSENTIA 3.0

License TypeScript Node.js

Shared TypeScript library for TWS/sera4 microservices. ESM, strict mode, full type declarations.

Requires Node.js ≥ 22.


What's new in 3.0

Breaking changes

  • Node.js ≥ 22 required (was ≥ 18 in 3.x)
  • TypeScript 6 (up from 5.3) — stricter inference rules may surface new type errors in consuming projects
  • All test files are now .ts only and use the *.test.ts convention; the .js test glob has been removed from the test runner

New

  • Biome replaces ad-hoc linting — single tool for lint, format, and import sorting (npm run check)
  • Husky pre-commit hook — Biome auto-fixes staged files before every commit
  • tsconfig.test.json — separate typecheck pass covering both ts/ and test/
  • .nvmrc pinned to Node 22

Improvements

  • Strict-mode cleanup across all modules — noImplicitAny, unused imports/variables enforced as errors
  • All source files use node: protocol for built-in imports (node:fs, node:path, etc.)
  • All file names normalised to kebab-case (e.g. dns-cache.ts, cycle-deployment-watcher.test.ts)
  • New test suites added for cache, hal, last-commit, paper-trail, safe-proxy, serializer, utils, s4-formatter, s4-pagination — 190 tests total

Setup

npm install
npm run build
npm test

Scripts

| Script | What it does | | ----------------------- | ---------------------------------------------- | | npm run build | Compile TypeScript → dist/ts/ | | npm test | Run all tests (Mocha + Chai, JUnit XML output) | | npm run test:coverage | Tests with c8 coverage HTML report | | npm run typecheck | Type-check without emitting | | npm run lint | Biome lint across ts/ and test/ | | npm run lint:fix | Biome lint with auto-fix | | npm run format | Biome format write pass | | npm run format:check | Biome format check (CI-safe) | | npm run check | Biome lint + format + import sort | | npm run check:fix | check with auto-fix |


Pre-commit hook

Husky runs Biome on staged files:

npx biome check --write --staged --no-errors-on-unmatched

The hook is wired via npm run prepare (runs automatically after npm install).


Modules

All source lives under ts/. Each module exports from its index.ts.

| Module | Export | Description | | ------------------- | ------------------------------------ | --------------------------------------------------------------------------- | | ts/logger | s4logger, S4Logger | Winston logger with trace/debug/info/warn/error levels | | ts/utils | utils | UUID validation, integer array parsing, route type detection, JSONB helpers | | ts/dns | DnsCache | Singleton DNS lookup cache with configurable TTL | | ts/health | HealthCheck | Health check endpoint utilities | | ts/cycle | CycleDeploymentWatcher | Watches Cycle environment file for deployment tag changes | | ts/constants | TWS_ROUTE_TYPES | Shared route type constants | | ts/helpers | helpers, TestServerWrapper | Misc helpers, test HTTP server wrapper | | ts/paginator | sqlPaginator, paginator | SQL and array pagination | | ts/cache | cache, Cache, CacheConfigError | Redis cache wrapper (fakeredis supported for tests) | | ts/prompts | setupCLIParser, cliSleep, etc. | CLI argument parsing and prompt utilities | | ts/dns-lb | DnsLbPool, dnsLbPool | DNS-aware round-robin load balancer across all instances of a Cycle deployment | | ts/safe-proxy | safeProxy, SafeProxy | Axios wrapper that forwards auth headers between services (supports LB) | | ts/queue | queue | AMQP publisher/subscriber with fanout support | | ts/last-commit | lastCommit | Read/write git branch + commit hash to a JSON file | | ts/hal | halDecorator | Decorates Sequelize models with HAL-style _links / _resource | | ts/formatter | S4Formatter | Error response formatting | | ts/serializer | serializer | Sequelize model serialization | | ts/paper-trail | paperTrail | Sequelize hook-based audit trail (diff + revision storage) |


dns-lb — DNS-aware round-robin load balancer

Background

Go's net/http resolves DNS on every request and distributes across all returned A records automatically. Node's http.Agent does not — it connection-pools by hostname, meaning once it resolves lockdepot.current-tag to one IP it keeps hitting that same container for the lifetime of the connection.

On Cycle.io there are two hostname forms:

| Form | Mechanism | Returns | |------|-----------|---------| | hostname.current-tag | Cycle discovery service (getaddrinfo) | 1 IP — lowest latency instance | | _hostname.current-tag | Cycle random selection (getaddrinfo) | 1 IP — random instance | | dig hostname.current-tag | Direct DNS query | All IPs — every instance |

The key is that dns.resolve4() (unlike dns.lookup()) bypasses getaddrinfo and queries DNS directly — the Node equivalent of dig. It returns the full A-record set for a hostname, which is all the IPs of every live container in the deployment.

DnsLbPool uses dns.resolve4() on the plain hostname (no underscore prefix needed) to get all IPs, feeds them into undici's BalancedPool (weighted round-robin), and diffs the upstream list in-place on each refresh so in-flight requests are never dropped when the deployment scales.

Node.js compatibility

Works on Node 22. undici 7.x is installed as an explicit npm dependency — it does not require Node 24. Node 24 is only significant because that's when Node will bundle undici 7 internally; until then it installs like any other package and works identically on Node 22.

Configuration

import { DnsLbPool, logger } from "@sera4/essentia";

// Singleton, configured once at startup. Pass your app logger so debug messages
// respect the same log level the rest of the app uses (S4_LOG_LEVEL=debug).
const lbPool = DnsLbPool.getInstance({
  logger,            // share the app logger — see "Logger injection" below
  debug: true,       // log pool state + timing on every request
  refreshIntervalMs: 5000,   // how often to re-resolve DNS (default: 5000ms)
  port: 80,          // target port for all upstreams (default: 80)
  protocol: "http:", // "http:" | "https:" (default: "http:")
  onStats: (stats) => {
    // called after each upstream diff — hook for Node 24 / undici 7 introspection
    // stats: { hostname, ips, connected, pending, running, queued }
    metrics.gauge("dns_lb.pool_size", stats.ips.length, { hostname: stats.hostname });
  },
});

Making HTTP requests directly

Use request() or requestUrl() when your service needs to call another service and get the full response body back (not proxied).

// request(hostname, options) — hostname + path style
const response = await lbPool.request("lockdepot.current-tag", {
  method: "GET",
  path: "/v3/locks",
  headers: { "tws-account-id": locals.account.id },
});
const locks = await response.body.json();

// requestUrl(url, options) — full URL style, compatible with SafeProxy call patterns
const response = await lbPool.requestUrl("http://lockdepot.current-tag/v3/locks", {
  method: "POST",
  headers: { "tws-account-id": locals.account.id },
  body: JSON.stringify({ serialNumber: "ABC123" }),
});
const created = await response.body.json();

response is Dispatcher.ResponseData from undici:

response.statusCode   // number
response.headers      // IncomingHttpHeaders
await response.body.json()     // parse JSON body
await response.body.text()     // parse text body
response.body                  // Readable stream for streaming responses

SafeProxy with load balancing

SafeProxy can optionally resolve hostnames via DnsLbPool before making its Axios calls. This keeps the same .data response API that callers already use.

import { SafeProxy, DnsLbPool, logger } from "@sera4/essentia";

// Create one load-balanced SafeProxy instance at startup
const lbPool = DnsLbPool.getInstance({ logger });
const lbProxy = new SafeProxy({ lbPool });

// Use exactly like safeProxy, but requests are now distributed
// across all instances of the target service
await lbProxy.post(res.locals, "http://identity-service.current-tag/v3/sessions", {
  data: { device, domain },
});
await lbProxy.delete(res.locals, "http://identity-service.current-tag/v3/sessions/abc123");

Under the hood, SafeProxy calls lbPool.resolveRoundRobin(hostname) to get a concrete IP, rewrites the URL to that IP, and sets a host header to the original hostname so the backend can route correctly.

The default safeProxy singleton (imported without new) is unchanged and backward compatible — it still uses Axios without load balancing.

Integration with http-proxy (streaming proxy / gateway pattern)

When using node:http-proxy or http-proxy-middleware as a transparent proxy, the library needs a concrete target URL — you can't plug in undici's BalancedPool directly. Use resolveRoundRobin() to pick one IP per request in round-robin order, then pass that IP as the proxy target.

resolveRoundRobin(hostname) returns a single IP from the pool on each call, cycling through all known IPs. The DNS pool refreshes on its own timer; the counter wraps by the current pool size so scale-up/down is handled automatically.

Replacing the gateway's server/helpers/dns.js:

// server/helpers/dns.js — replace the entire file with this
import { logger, DnsLbPool } from "@sera4/essentia";
import configs from "../../configurations/server-config.js";

const lbPool = DnsLbPool.getInstance({
  logger,
  debug: configs.debug_dns,   // respects the existing S4_DEBUG_DNS env flag
  refreshIntervalMs: 5000,
});

// resolveTarget: same API as before — returns http://[ip]:port/path
// The only difference is dns.resolve4() now selects from ALL instance IPs in round-robin.
async function resolveTarget(urlOrHostname, keepPath = false) {
  const parsed = new URL(urlOrHostname);
  try {
    if (process.env.NODE_ENV === "test") {
      return `${parsed.protocol}//localhost:${parsed.port || 80}${keepPath ? parsed.pathname : ""}`;
    }
    const ip = await lbPool.resolveRoundRobin(parsed.hostname);
    const hostPart = ip.includes(":") ? `[${ip}]` : ip; // wrap IPv6
    return `${parsed.protocol}//${hostPart}:${parsed.port || 80}${keepPath ? parsed.pathname : ""}`;
  } catch (e) {
    logger.warn(`DNS resolution failed for ${urlOrHostname}, falling back: ${e.message}`);
    return urlOrHostname;
  }
}

// resolveIdentityEndpoint: unchanged API, now load-balanced
const resolveIdentityEndpoint = async () => {
  try {
    return await resolveTarget(configs.s4_identity_endpoint, true);
  } catch (error) {
    logger.error(`Failed to resolve identity endpoint:`, error.message);
    return configs.s4_identity_endpoint;
  }
};

// forceCachePurge: triggers an immediate DNS re-resolution.
// Called when a Cycle deployment-change event arrives via the queue.
export function forceCachePurge(hostname) {
  lbPool.forceRefresh(hostname).catch((err) => {
    logger.warn(`Force DNS refresh failed for ${hostname ?? "all"}:`, err.message);
  });
}

// purgeExpiredCache: DnsLbPool self-manages TTL internally — this is a no-op kept
// for backward compatibility with background-tasks.js.
export function purgeExpiredCache() {}

export const dnsCache = new Map();        // kept for backward compat; no longer populated
export const PURGE_INTERVAL_MS = 60 * 2 * 1000; // kept for backward compat

export default {
  resolveTarget,
  resolveIdentityEndpoint,
  forceCachePurge,
  dnsCache,
  purgeExpiredCache,
  PURGE_INTERVAL_MS,
};

Nothing else in the gateway changes — all callers of dns.resolveTarget(), dns.resolveIdentityEndpoint(), and dns.forceCachePurge() continue to work without modification. The background-tasks.js purge interval can be left as-is (calling a no-op is harmless) or removed.

forceRefresh — responding to deployment change events

When the queue delivers a cache.dns.* message signalling that a deployment has changed, call forceRefresh to re-resolve DNS immediately rather than waiting for the next TTL tick:

// Force re-resolution of one hostname
await lbPool.forceRefresh("lockdepot.current-tag");

// Force re-resolution of all tracked hostnames
await lbPool.forceRefresh();

Logger injection

Pass your application's configured S4Logger instance so DnsLbPool emits at the same level the rest of the service uses. Without injection, a new logger is created with service="dns-lb-pool".

// In server startup, before any requests are handled:
import { DnsLbPool, logger } from "@sera4/essentia";
logger.configure({ service: "gateway", level: "debug" });

DnsLbPool.getInstance({ logger, debug: true });
// Now dns-lb debug messages appear when S4_LOG_LEVEL=debug,
// and are suppressed at info/warn just like every other log line.

When debug: true is set, every request logs:

  • Before: dns-lb: GET lockdepot.current-tag/v3/locks → pool [10.0.0.1, 10.0.0.2, 10.0.0.3]
  • After: dns-lb: lockdepot.current-tag/v3/locks ← 200 (12ms)
  • Scale events: dns-lb: +upstream 10.0.0.4 for lockdepot.current-tag

Inspecting the pool

lbPool.getIps("lockdepot.current-tag")  // → ["10.0.0.1", "10.0.0.2", "10.0.0.3"]

Cleanup

// On process shutdown, drain in-flight requests and close connections:
await lbPool.destroy();

Logger environment policy

S4Logger.resolveLogLevel() reads S4_LOG_LEVEL; when it is set to a valid level, that value always wins. When it is unset or invalid, logging defaults to info.

  • S4_LOG_LEVEL=debug: emit debug and above
  • S4_LOG_LEVEL=info: emit info and above
  • unset or invalid: default to info

Usage

import { logger, utils, DnsCache, DnsLbPool, SafeProxy, queue, safeProxy } from "@sera4/essentia";

// Logger
logger.configure({ level: "info", service: "my-service", format: "json" });
logger.info("Starting");

// Utils
const isValid = utils.isValidUuidV4("26d61a82-3587-4875-98a8-e950e1bf2350");

// DnsCache — single-IP lookup with TTL cache (uses dns.lookup internally)
const dnsCache = DnsCache.getInstance();
const ip = await dnsCache.resolveTarget("https://example.com");

// DnsLbPool — full A-record round-robin across all instances (uses dns.resolve4 internally)
const lbPool = DnsLbPool.getInstance({ logger, debug: true });

// Direct HTTP call — load balanced
const response = await lbPool.requestUrl("http://lockdepot.current-tag/v3/locks");
const locks = await response.body.json();

// SafeProxy with load balancing
const lbProxy = new SafeProxy({ lbPool });
await lbProxy.post(res.locals, "http://identity-service.current-tag/v3/sessions", { data: {} });

// SafeProxy without load balancing (original behaviour)
await safeProxy.post(res.locals, "http://other-service/endpoint", { data: {} });

// Queue
await queue.openConnection({ connectionUrl: "amqp://localhost" });
await queue.publishMessage("my-exchange", "routing.key", { payload: true });

Testing

Tests run manually — no CI pipeline yet.

npm test                          # all tests
npm run test:coverage             # HTML coverage report in coverage/
npm test -- --grep "DnsCache"     # run a specific suite

Test files live in test/, mirroring the ts/ structure. Files use the .test.ts naming convention.


License

Proprietary. All rights reserved.