Documentation/Learn/common
In this section

@omnitron-dev/common

pnpm add @omnitron-dev/common

A focused TypeScript utility library. Type predicates with proper narrowing, promise lifecycle helpers, ergonomic object tools, small high-performance data structures. Works in Node.js and Bun.

Verified against packages/common/src/.

Modules

ModuleSurface
predicates50+ type predicates (isString, isPlainObject, isPromise, …) + platform flags
promisedefer, delay, timeout, retry, props, runExclusive, promisify, callbackify
omitDeep / path-based / predicate-based property omission
entriesTyped entries / keys / values
primitivesnoop, identity, truly, falsely, arrify
list-bufferO(1) unbounded FIFO queue
timed-mapTTL Map (auto-expiring entries)
p-limitPromise concurrency limiter
decimalLossless decimal arithmetic

Type predicates

50+ predicates with proper is X return types that narrow:

import {
  isString, isNumber, isBoolean, isArray, isFunction,
  isPlainObject, isObject, isPromise, isError, isDate,
  isBuffer, isNull, isUndefined, isNil, isClass, isAsyncFunction,
} from '@omnitron-dev/common';

function process(value: unknown) {
  if (isString(value))     return value.toUpperCase();   // ← narrowed to string
  if (isPlainObject(value)) return Object.keys(value);   // ← narrowed to object
  if (isPromise(value))    return value.then(handle);    // ← narrowed to Promise
  return value;
}

Plus platform flags (set once at import):

import { isWindows, linux, darwin, isNodejs } from '@omnitron-dev/common';

if (darwin && isNodejs) {
  // macOS + Node.js — both true / false constants
}

Promise helpers

delay(ms, value?, opts?)

await delay(1_000);                                  // sleep 1s
const result = await delay(500, 'hello');           // sleep 500ms then resolve 'hello'
await delay(60_000, undefined, { unref: true });    // doesn't block event loop

defer() — externally-controlled promise

import { defer } from '@omnitron-dev/common';

const d = defer<User>();

// somewhere else, once the value is known:
d.resolve(currentUser);
// or:
d.reject(new Error('cancelled'));

// wherever it is needed:
const user = await d.promise;

Useful when you need to expose .resolve / .reject to code that isn't inside the promise's executor.

timeout(promise, ms, opts?)

import { timeout } from '@omnitron-dev/common';

const result = await timeout(
  fetchData(),
  5_000,
  { signal: abortController.signal },
);

Wraps a promise; rejects with Error('Timeout of 5000ms exceeded') if it doesn't settle in time. Options are { unref?, signal? } — signal lets external code abort early (rejects with Error('Timeout aborted')).

retry(callback, opts | max)

import { retry } from '@omnitron-dev/common';

const data = await retry(
  ({ current }) => fetch(url).then(r => r.json()),   // callback gets { current } attempt no.
  {
    max:             5,           // max attempts
    timeout:         8_000,       // per-attempt timeout (ms), optional
    backoffBase:     500,         // base delay (ms), default 100
    backoffExponent: 2,           // multiplier, default 1.1
    match:           [NetworkError, /ECONN/],  // only retry matching errors
    name:            'fetchJson',
    report:          (msg, opts, err) => log(msg),
  },
);

// Shorthand — just a max attempt count:
await retry(() => doThing(), 3);

Exponential backoff (backoffBase * backoffExponent^(attempt-1)), optional per-attempt timeout, and match to restrict which errors trigger a retry (strings, RegExps, or error classes). Pass a bare number to set max with defaults for the rest.

props(obj)

const result = await props({
  user:    fetchUser(id),
  posts:   fetchPosts(id),
  friends: fetchFriends(id),
});
// { user: User, posts: Post[], friends: User[] } — all awaited

Promise.all for objects — preserves keys.

runExclusive(resource, operation)

import { runExclusive } from '@omnitron-dev/common';

const output = await runExclusive(renderer, async () => {
  renderer.configure(options);
  return renderer.render(source);
});

Calls sharing the same object run in arrival order; different objects remain independent. The next operation runs even when its predecessor rejects. This coordinates mutable library instances inside one JavaScript runtime, as used by Lumen and Prism for a shared Mermaid engine. Do not await another exclusive call for that same object from inside its operation: the queue is not reentrant. It provides neither inter-process locking nor transaction rollback.

promisify(fn) / callbackify(fn)

Bridge Node-style callbacks ↔ promises.

import { promisify } from '@omnitron-dev/common';
import { exec } from 'node:child_process';

const execAsync = promisify(exec);
const { stdout } = await execAsync('git log');

Object tools

omit(obj, keys | predicate | regex | string, opts?)

import { omit } from '@omnitron-dev/common';

omit(user, ['password', 'salt']);                       // remove specific keys
omit(payload, (key, value) => value == null);           // predicate (key, value, object)
omit(state, /^_/);                                       // regex on key
omit(deep, 'user.credentials', { path: true });         // dotted path (requires { path: true })

The predicate receives (key, value, object). Dotted-path removal requires the third-arg flag { path: true }; without it a string is treated as an exact key name.

entries(obj, opts?) / keys(obj, opts?) / values(obj, opts?)

import { entries, keys } from '@omnitron-dev/common';

entries({ a: 1, b: 2 });                           // [['a', 1], ['b', 2]]
keys(obj, { all: true });                           // include non-enumerable + prototype keys
keys(obj, { followProto: true });                   // walk the prototype chain

Options are { enumOnly = true, followProto = false, all = false }. These operate on plain objects (own/prototype string keys) — they do not iterate Map/Set entries.

Data structures

ListBuffer<T> — O(1) FIFO queue

import { ListBuffer } from '@omnitron-dev/common';

const buffer = new ListBuffer<LogEntry>();
buffer.push(entry);                  // O(1) enqueue
const oldest = buffer.shift();       // O(1) dequeue (undefined when empty)
buffer.length;                        // current count (getter)
buffer.clear();                       // empty it

Backed by a singly-linked list — no array reallocation. It is an unbounded queue: there is no capacity argument and no automatic eviction; size it by shift()-ing as you consume.

TimedMap<K, V> — auto-expiring map

import { TimedMap } from '@omnitron-dev/common';

const cache = new TimedMap<string, User>(60_000);      // default 60s TTL per key
cache.set('u_42', user);                               // expires after 60s
cache.set('u_43', user, undefined, 300_000);           // custom 5min TTL (callback, ttl)
cache.get('u_42');                                     // returns User or undefined

Constructor is new TimedMap(timeoutMs?, callback?); on expiry the key is deleted (or callback(key) runs if provided). Each entry has its own setTimeout — there is no shared GC sweep and no .dispose(). Pass a per-key (callback, timeout) to set() to override the default.

pLimit(n) — concurrency limiter

import { pLimit } from '@omnitron-dev/common';

const limit = pLimit(5);
const results = await Promise.all(
  urls.map(url => limit(() => fetch(url))),
);
// At most 5 fetches in flight at any moment.

Decimal — lossless decimal arithmetic

import { Decimal } from '@omnitron-dev/common';

const total = Decimal.from('0.1').add('0.2');    // exact 0.3 (use Decimal.from, not new)
const tax   = Decimal.from(amount).multiplyBy('0.08');
const cents = total.toString();                   // string
const num   = total.toNumber();                   // number (lossy — use sparingly)

Construct via the static Decimal.from(value, precision?) / Decimal.zero() (the constructor is private). Methods: add, subtract, multiply(n) / multiplyBy(other), divide(n) / divideBy(other), abs, negate, comparisons (equals, gt, gte, lt, lte, compare), round/floor/ceil(decimalPlaces), and toString / toNumber / toBigInt.

For money / currency. Never trust Number for those.

The standalone helpers

The module also exports 31 string-in / string-out functions for fixed-precision maths. They are the whole surface, listed rather than sampled, because an incomplete list of money helpers is an invitation to write a thirty-second one by hand. Every one takes a trailing precision argument defaulting to DEFAULT_PRECISION (12).

Arithmetic
addDecimals(a, b) · subtractDecimals(a, b)both operands are strings
multiplyDecimal(a, multiplier: number)scale by a plain number
multiplyDecimals(a, b: string)multiply two decimal strings
divideDecimal(a, divisor: number)divide by a plain number; throws on 0
divideDecimals(a, b: string)divide two decimal strings
sumDecimals(values: string[])sum an array
percentOf(amount, percent)percentOf('100', '2.5') → '2.500000000000'
absDecimal(v) · minDecimal(a, b) · maxDecimal(a, b) · zero()

Watch the singular/plural pair. multiplyDecimal takes a number; multiplyDecimals takes a decimal string. Same for divideDecimal / divideDecimals. One letter apart and a different second argument — the compiler catches a swap, an any does not.

Comparison
compareDecimals(a, b)-1 | 0 | 1
isGreater · isGreaterOrEqual · isLess · isLessOrEqual (a, b)
isZero(v) · isPositive(v) · isNegative(v)
Rounding and formatting
roundDecimal(v, decimalPlaces) · floorDecimal · ceilDecimal
formatDecimal(v)normalise to the precision
parseDecimal(v: string | number)accepts a number at the boundary
Crypto units
satoshisToBtc(sat: bigint | number | string) → string
btcToSatoshis(btc: string) → bigint
atomicToXmr(atomic: bigint | number | string) → string
xmrToAtomic(xmr: string) → bigint
Validation
isValidDecimal(v)format only
validateAmount(amount, { minAmount?, maxAmount?, precision?, allowZero?, allowNegative? })returns { valid, error? }; allowZero defaults true, allowNegative false

Results are padded to the precision, not trimmed: percentOf('100', '2.5') is '2.500000000000', not '2.5'. Compare with compareDecimals or isZero, never with === against a literal you typed by hand.

The unit converters are the exception — they answer in their coin's own precision, so satoshisToBtc(100000000n) is '1.00000000' (8 places), not 12.

They return bigint in the atomic direction on purpose: satoshis and piconeros exceed Number.MAX_SAFE_INTEGER for large amounts, so a number there is the same lossy trap Decimal exists to avoid.

Native process identity

The Node-only @omnitron-dev/common/process-identity entry combines a host boot identity with the native process birth. Omnitron ownership checks and Lumen publication recovery use the same implementation. The browser-safe root does not load native process probes.

import {
  readProcessLifetime,
  compareProcessLifetimes,
} from '@omnitron-dev/common/process-identity';

const before = readProcessLifetime(process.pid);
const after = readProcessLifetime(process.pid);
if (before.state === 'alive' && after.state === 'alive') {
  console.log(compareProcessLifetimes(before.identity, after.identity));
}

readProcessLifetime(pid) returns alive with an opaque identity, dead when native observation proves the PID absent, or unknown when it cannot establish either. Comparisons return same, different or unknown. Never interpret an unknown observation as permission to take another process's resources.

Linux uses the kernel boot UUID and process start ticks. macOS uses the boot session UUID and UTC native process birth. Legacy local-time births cannot prove a process replacement from a changed timestamp alone. These probes do not yet support Windows. Keep policy, cleanup and lifecycle decisions in the caller.

Primitives

import { noop, identity, truly, falsely, arrify } from '@omnitron-dev/common';

noop();                                  // returns undefined
identity(x);                              // returns x
truly();                                  // returns true
falsely();                                // returns false
arrify(value);                            // [value] if not array, else value
arrify(null);                             // []
arrify([1, 2]);                          // [1, 2]

arrify is the one you'll reach for most — coalescing "single-or-many" args into a consistent array.

Use across the stack

  • Titan's lifecycle hooks use delay for graceful drain.

  • Netron's middleware uses retry for transient errors.

  • Omnitron's daemon uses TimedMap for the token cache.

  • titan-cache ships its own LRU but uses TimedMap for auxiliary structures.

  • titan-events uses defer for the request/response pattern.

See also