| name | welshman-lib |
| description | Use this skill when working with @welshman/lib: general-purpose utilities including LRU cache, EventEmitter, Deferred promises, TaskQueue, URL normalization, or other standalone helpers. |
welshman/lib — General Utilities
@welshman/lib is a lightweight TypeScript utility library that forms the foundation of the welshman nostr stack. It provides common helpers used across all sibling packages: array/object manipulation, numeric helpers, async primitives, caching, event emission, and encoding utilities. It depends on @scure/base (for bech32/utf8 encoding) and events (Node.js EventEmitter polyfill).
Installation
npm install @welshman/lib
pnpm add @welshman/lib
Key Exports
Deferred Promises
| Export | Description |
|---|
Deferred<T, E> | Type: a Promise<T> with .resolve(T) and .reject(E) methods attached |
defer<T, E>() | Creates a Deferred<T, E> — a promise with exposed .resolve() and .reject() |
makePromise<T, E>(executor) | Creates a strongly-typed promise with typed error |
E defaults to T when omitted. defer<void>() for a signal-style deferred. thunk.complete in @welshman/app is a Deferred<void>.
import { defer } from '@welshman/lib'
const ready = defer<void>()
socket.on('open', () => ready.resolve())
await ready
EventEmitter
| Export | Description |
|---|
Emitter | Extends Node.js EventEmitter; all events also fire on the '*' listener with the event name prepended |
import { Emitter } from '@welshman/lib'
const bus = new Emitter()
bus.on('*', (eventType, ...args) => console.log(eventType, args))
bus.emit('login', { pubkey: '...' })
LRU Cache
| Export | Description |
|---|
LRUCache<K, V> | LRU cache; evicts least-recently-used entries when full |
cached(options) | Memoizes a function with an LRU backing cache; exposes .cache and .pop() |
simpleCache(getValue) | Minimal memoization wrapper with default settings |
import { LRUCache, cached } from '@welshman/lib'
const cache = new LRUCache<string, number>(100)
cache.set('x', 42)
cache.get('x')
cache.pop('x')
const getProfile = cached({
maxSize: 500,
getKey: ([pubkey]: [string]) => pubkey,
getValue: ([pubkey]: [string]) => fetchProfile(pubkey),
})
getProfile.pop(pubkey)
Task Queue
| Export | Description |
|---|
TaskQueue<Item> | Processes items asynchronously in configurable batches |
Options: batchSize, batchDelay (ms), processItem. Methods: .push(item), .remove(item), .start(), .stop(), .clear(), .process(), .subscribe(cb).
import { TaskQueue } from '@welshman/lib'
const queue = new TaskQueue<string>({
batchSize: 10,
batchDelay: 0,
processItem: async (item) => { },
})
queue.start()
queue.push('task-1')
URL Normalization
| Export | Description |
|---|
normalizeUrl(url, options?) | Normalizes a URL string (ported from sindresorhus/normalize-url) |
stripProtocol(url) | Removes the protocol prefix (http://, wss://, etc.) |
displayUrl(url) | Strips protocol, www., and trailing slash for display |
displayDomain(url) | Extracts just the domain from a URL |
import { normalizeUrl, displayUrl, displayDomain } from '@welshman/lib'
normalizeUrl('sindresorhus.com/about.html#contact', { stripHash: true })
displayUrl('https://www.example.com/path/')
displayDomain('relay.damus.io/path')
Note: normalizeUrl defaults to http:// protocol. Pass { defaultProtocol: 'https' } if needed.
Async Utilities
| Export | Description |
|---|
sleep(ms) | Returns a promise that resolves after ms milliseconds |
yieldThread() | Yields to the event loop (microtask break) |
poll(options) | Polls until a condition is met or an AbortSignal fires; options: { signal, condition, interval? } |
throttle(ms, fn) | Returns a throttled version of fn |
throttleWithValue(ms, fn) | Throttled function that returns the cached return value between updates |
batch(t, fn) | First call fires fn([item]) immediately; subsequent calls within t ms are collected and fn is called with all accumulated items |
batcher(t, execute) | Collects calls for t ms, then calls execute with all accumulated requests; each individual call returns a Promise<U> resolved with its result from the batch. Unlike batch, the first call is also deferred — nothing fires immediately. |
race(threshold, promises) | Resolves when threshold fraction of promises complete |
Timestamp / Time Constants
| Export | Description |
|---|
MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR | Duration constants in seconds |
LOCALE | User's default locale string |
TIMEZONE | User's timezone offset string (e.g. +05:30) |
now() | Current Unix timestamp in seconds |
ago(unit, count?) | Unix timestamp from count units ago — e.g. ago(DAY, 7) |
int(unit, count?) | Multiplies a time unit by count — e.g. int(HOUR, 2) = 7200 |
ms(seconds) | Converts seconds to milliseconds |
secondsToDate(ts) / dateToSeconds(date) | Convert between Unix seconds and Date |
createLocalDate(dateString, timezone?) | Parses a date string as a local date in the given timezone |
formatTimestamp(ts) | Formats Unix seconds as a short datetime string |
formatTimestampAsDate(ts) | Formats Unix seconds as a long date string |
formatTimestampAsTime(ts) | Formats Unix seconds as a time string |
formatTimestampRelative(ts) | Formats Unix seconds as "x minutes ago" |
Note: All time constants are in seconds, not milliseconds. Use ms(n) to convert for setTimeout.
Number Utilities
| Export | Description |
|---|
ensureNumber(x) | parseFloat(x) — accepts string | number |
num(x) | Returns x || 0 — converts undefined to 0 |
add(x, y) / sub(x, y) / mul(x, y) / div(x, y) | Arithmetic with undefined-safe operands |
inc(x) / dec(x) | Increment / decrement (undefined-safe) |
lt(x, y) / lte(x, y) / gt(x, y) / gte(x, y) | Comparisons (undefined-safe) |
max(xs) / min(xs) / sum(xs) / avg(xs) | Aggregates over (number | undefined)[] |
between([low, high], n) | n > low && n < high (exclusive) |
within([low, high], n) | n >= low && n <= high (inclusive) |
clamp([min, max], n) | Constrains n to the range |
round(precision, x) | Rounds to precision decimal places |
Array / Sequence Utilities
All return new arrays — no mutation.
| Export | Description |
|---|
first(xs) / last(xs) | First/last element (undefined if empty) |
ffirst(xs) | First element of the first iterable in a nested iterable |
take(n, xs) / drop(n, xs) | Slice from start / drop from start |
concat(...xs) | Flattens vararg arrays into one, skipping any argument that is undefined |
append(x, xs) / prepend(x, xs) | Add element to end / start |
remove(x, xs) | Remove all occurrences of x |
removeAt(i, xs) | Remove element at index i |
splitAt(n, xs) | Split into [xs.slice(0, n), xs.slice(n)] |
insertAt(n, x, xs) | Insert x at index n |
replaceAt(n, x, xs) | Replace element at index n with x |
uniq(xs) / uniqBy(f, xs) | Deduplicate |
sort(xs) | Sorted copy (natural order) |
sortBy(f, xs) | Sort by key function |
groupBy(f, xs) | Returns Map<K, T[]> |
indexBy(f, xs) | Returns Map<K, T> (last item wins per key) |
countBy(f, xs) | Returns Map<K, number> |
partition(f, xs) | Split into [passing, failing] |
chunk(n, xs) | Split into fixed-size chunks of length n |
chunks(n, xs) | Split into exactly n chunks |
toggle(x, xs) | Add if absent, remove if present (pure) |
union(a, b) / intersection(a, b) / difference(a, b) / without(a, b) | Set operations |
sample(n, xs) / shuffle(xs) / choice(xs) | Random selection / shuffle / single random pick |
flatten(xs) | Flatten one level |
ensurePlural(x) | Wraps a value in [x] if it isn't already an array |
removeUndefined(xs) | Filters out undefined values |
overlappingPairs(xs) | Returns [[xs[0],xs[1]], [xs[1],xs[2]], ...] |
range(a, b, step?) | Generator yielding numbers from a to b (exclusive) |
enumerate(xs) | Generator yielding [index, item] tuples |
pluck<T>(k, xs) | Maps xs to xs[k] |
fromPairs(pairs) | Creates an object from [key, value] tuples |
initArray(n, f) | Creates an array of length n using generator f |
isIterable(x) / toIterable(x) | Check / wrap as iterable |
map(f, xs) / filter(f, xs) / reject(f, xs) | Iterable-safe versions (accept any Iterable<T>) |
find(f, xs) / some(f, xs) | Iterable-safe find / any-match |
Object Utilities
| Export | Description |
|---|
isPojo(obj) | Returns true if value is a plain object (not class instance, null, or array) |
pick(keys, obj) / omit(keys, obj) | Include / exclude keys |
omitVals(vals, obj) | Remove entries whose value is in vals |
filterVals(f, obj) | Keep entries where f(value) is truthy |
mapKeys(f, obj) / mapVals(f, obj) | Transform keys or values |
mergeLeft(a, b) / mergeRight(a, b) | Shallow merge — left/right wins on conflicts |
deepMergeLeft(a, b) / deepMergeRight(a, b) | Deep merge — left/right wins on conflicts |
switcher(key, map) | Lookup with implicit map.default fallback |
mapPop(k, m) | Gets and deletes key from a Map<K, T> — returns T | undefined |
Note: mergeLeft(a, b) means a wins — it spreads b first, then a on top.
TypeScript Utility Types
import type { Override, MakeOptional, MakeNonOptional, Obj, Maybe, MaybeAsync } from '@welshman/lib'
type UserWithRole = Override<User, { role: 'admin' | 'user' }>
type DraftUser = MakeOptional<User, 'id' | 'createdAt'>
type FullUser = MakeNonOptional<User>
type AnyRecord = Obj
type MaybeStr = Maybe<string>
Functional / Combinator Helpers
| Export | Description |
|---|
noop | No-op function |
identity(x) | Returns x unchanged |
always(x) | Returns a function that always returns x |
not(x) | Logical NOT |
complement(f) | Returns (...args) => !f(...args) |
tap(f) | Returns (x) => { f(x); return x } — runs a side effect and passes the value through |
bind(f, ...args) | Partially applies f with leading args |
equals(a, b) | Deep equality (handles arrays, Sets, plain objects) |
tryCatch(f, onError?) | Calls f, swallows errors, returns undefined on failure |
thrower(message) | Returns a function that throws new Error(message) when called |
once(f) | Wraps f so it only executes once |
memoize(f) | Single-slot memoization: caches last call; re-runs when args change |
call(f) | Calls f() immediately — IIFE alternative; useful with async |
ifLet(x, f) | Calls f(x) only if x is defined |
doLet(x, f) | Calls f(x) and returns the result — scoped binding without a variable |
isDefined(x) / isUndefined(x) / assertDefined(x) | undefined checks (not null) |
Curried Collection Helpers
Useful as .filter() / .map() callbacks:
| Export | Description |
|---|
eq(v) / ne(v) | x => x === v / x => x !== v |
prop(k) | x => x[k] — pluck a property |
propIn(k, xs) | x => xs.includes(x[k]) — property is in list |
nth(i) | xs => xs[i] — element at index |
nthEq(i, v) | xs => xs[i] === v |
nthNe(i, v) | xs => xs[i] !== v |
nthIn(i, vs) | xs => vs.includes(xs[i]) |
nthNotIn(i, vs) | xs => !vs.includes(xs[i]) |
spec(values) | x => all key-value pairs in values match x |
member(xs) | x => xs.includes(x) |
assoc(k, v) | obj => ({ ...obj, [k]: v }) — add/update property |
dissoc(k) | obj => omit([k], obj) — remove property |
import { eq, prop, nth, nthEq, nthIn, nthNotIn, spec, member, assoc, dissoc } from '@welshman/lib'
events.filter(spec({ kind: 1 }))
events.map(prop('id'))
tags.filter(nthEq(0, 'p'))
tags.filter(nthIn(0, ['p', 'e']))
tags.filter(nthNotIn(0, ['p', 'e']))
items.filter(member(['a', 'b']))
items.map(assoc('seen', true))
items.map(dissoc('secret'))
Bech32 / Hex / Binary Encoding
| Export | Description |
|---|
hexToBech32(prefix, hex) | Encodes hex string to bech32 (e.g. npub, note) |
bech32ToHex(b32) | Decodes bech32 to hex |
bytesToHex(buffer) | ArrayBuffer | Uint8Array to hex string |
hexToBytes(hex) | Hex string to Uint8Array |
sha256(data) | SHA-256 hash of binary data — async, returns hex string |
textEncoder | Shared TextEncoder instance |
textDecoder | Shared TextDecoder instance |
JSON / Storage / Network
| Export | Description |
|---|
parseJson(str) | Safe JSON.parse — returns undefined on error or empty input |
getJson(key) / setJson(key, val) | localStorage get/set with JSON serialization |
fetchJson(url, opts?) | Fetch JSON with optional method/headers/body |
postJson(url, data, opts?) | POST JSON to a URL |
uploadFile(url, file) | Upload a File object via multipart/form-data POST |
on(target, event, cb) | Type-safe .on() wrapper — returns an unsubscribe () => void |
Randomness / IDs
| Export | Description |
|---|
randomId() | Generates a random string ID |
randomInt(min?, max?) | Random integer in range (inclusive; default 0–9) |
String Utilities
| Export | Description |
|---|
ellipsize(s, len, suffix?) | Truncates at word boundary with an ellipsis suffix (default "...") |
displayList(xs, conj?, n?) | Oxford-comma list — e.g. "a, b, and c" |
hash(s) | Numeric hash from a string |
Map / Set Helpers
import { addToKey, pushToKey, addToMapKey, pushToMapKey } from '@welshman/lib'
addToKey(byTag, 'p', pubkey)
pushToKey(byKind, '1', eventId)
addToMapKey(m, relay, eventId)
pushToMapKey(m, relay, eventId)
Common Patterns
Pattern 1 — Batching writes (IndexedDB / relay)
batch(t, f) — first call fires immediately; subsequent calls within t ms are accumulated and flushed together.
import { batch, on } from '@welshman/lib'
import type { RepositoryUpdate } from '@welshman/net'
on(
repository,
'update',
batch(3000, async (updates: RepositoryUpdate[]) => {
const toAdd = updates.flatMap(u => u.added)
const toRemove = new Set(updates.flatMap(u => u.removed))
const tx = db.transaction('events', 'readwrite')
await Promise.all([
...toAdd.map(e => tx.store.put(e)),
...Array.from(toRemove).map(id => tx.store.delete(id)),
tx.done,
])
}),
)
Pattern 2 — Grouping and indexing nostr events
import { groupBy, indexBy, sortBy } from '@welshman/lib'
const byKind = groupBy((e) => e.kind, events)
const byId = indexBy((e) => e.id, events)
const sorted = sortBy((e) => -e.created_at, events)
Pattern 3 — Relative timestamp display
import { now, ago, DAY, formatTimestampRelative } from '@welshman/lib'
const recentEvents = events.filter(e => e.created_at > ago(DAY, 7))
const label = formatTimestampRelative(event.created_at)
Pattern 4 — Subscribing to EventEmitter-based objects with on
on(target, event, handler) returns an unsubscribe function:
import { on } from '@welshman/lib'
import { repository } from '@welshman/app'
const unsub = on(repository, 'update', updates => {
console.log('added', updates.flatMap(u => u.added).length, 'events')
})
unsub()
Pattern 5 — IIFE alternative with call
import { call } from '@welshman/lib'
call(async () => {
const data = await fetchJson('/something')
})
Integration Notes
- All welshman packages depend on
@welshman/lib — Deferred, Emitter, time constants, and collection utilities are shared across @welshman/net, @welshman/store, @welshman/util, etc.
@welshman/net uses Emitter (via Tracker, Repository, WrapManager), batch, and LRUCache internally. Socket extends Node's built-in EventEmitter directly.
@welshman/app thunks use Deferred<void> — thunk.complete resolves when all relays have responded or timed out.
batcher is used in @welshman/net for deduplicating concurrent fetch requests — pass it an execute function that returns results in the same order as its inputs.