@omnitron-dev/common
pnpm add @omnitron-dev/commonA 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
| Module | Surface |
predicates | 50+ type predicates (isString, isPlainObject, isPromise, …) + platform flags |
promise | defer, delay, timeout, retry, props, runExclusive, promisify, callbackify |
omit | Deep / path-based / predicate-based property omission |
entries | Typed entries / keys / values |
primitives | noop, identity, truly, falsely, arrify |
list-buffer | O(1) unbounded FIFO queue |
timed-map | TTL Map (auto-expiring entries) |
p-limit | Promise concurrency limiter |
decimal | Lossless 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 loopdefer() — 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 awaitedPromise.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 chainOptions 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 itBacked 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 undefinedConstructor 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
delayfor graceful drain.Netron's middleware uses
retryfor transient errors.Omnitron's daemon uses
TimedMapfor the token cache.titan-cacheships its own LRU but usesTimedMapfor auxiliary structures.titan-eventsusesdeferfor the request/response pattern.
See also
eventemitter — sibling utility, async emission patterns
cuid — ID generation
msgpack — wire format