---
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

```bash
npm install @welshman/lib
# or
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>`.

```typescript
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 |

```typescript
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 |

```typescript
import { LRUCache, cached } from '@welshman/lib'

const cache = new LRUCache<string, number>(100)
cache.set('x', 42)
cache.get('x')  // 42
cache.pop('x')  // 42 and removes entry

const getProfile = cached({
  maxSize: 500,
  getKey: ([pubkey]: [string]) => pubkey,
  getValue: ([pubkey]: [string]) => fetchProfile(pubkey),
})
getProfile.pop(pubkey) // invalidate one entry
```

### 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)`.

```typescript
import { TaskQueue } from '@welshman/lib'

const queue = new TaskQueue<string>({
  batchSize: 10,
  batchDelay: 0,
  processItem: async (item) => { /* handle */ },
})
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 |

```typescript
import { normalizeUrl, displayUrl, displayDomain } from '@welshman/lib'

normalizeUrl('sindresorhus.com/about.html#contact', { stripHash: true })
// => 'http://sindresorhus.com/about.html'
displayUrl('https://www.example.com/path/')  // => 'example.com/path'
displayDomain('relay.damus.io/path')         // => 'relay.damus.io'
```

> **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

```typescript
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          // Record<string, any>
type MaybeStr = Maybe<string> // string | undefined
```

### 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 |

```typescript
import { eq, prop, nth, nthEq, nthIn, nthNotIn, spec, member, assoc, dissoc } from '@welshman/lib'

events.filter(spec({ kind: 1 }))          // kind === 1
events.map(prop('id'))                     // pluck id
tags.filter(nthEq(0, 'p'))               // tags where tag[0] === 'p'
tags.filter(nthIn(0, ['p', 'e']))         // tags where tag[0] is 'p' or 'e'
tags.filter(nthNotIn(0, ['p', 'e']))      // tags where tag[0] is neither
items.filter(member(['a', 'b']))           // items in the set
items.map(assoc('seen', true))            // add property
items.map(dissoc('secret'))               // remove property
```

### 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

```typescript
import { addToKey, pushToKey, addToMapKey, pushToMapKey } from '@welshman/lib'

// Object-keyed (Record<string, Set<T>> / Record<string, T[]>)
addToKey(byTag, 'p', pubkey)     // adds to Set at key 'p'
pushToKey(byKind, '1', eventId)  // appends to array at key '1'

// Map-keyed (Map<K, Set<T>> / Map<K, T[]>)
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.

```typescript
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

```typescript
import { groupBy, indexBy, sortBy } from '@welshman/lib'

const byKind = groupBy((e) => e.kind, events)       // Map<number, Event[]>
const byId   = indexBy((e) => e.id, events)         // Map<string, Event>
const sorted = sortBy((e) => -e.created_at, events) // newest first
```

### Pattern 3 — Relative timestamp display

```typescript
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) // "3 hours ago"
```

### Pattern 4 — Subscribing to EventEmitter-based objects with `on`

`on(target, event, handler)` returns an unsubscribe function:

```typescript
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')
})

// Later:
unsub()
```

### Pattern 5 — IIFE alternative with `call`

```typescript
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.
