@toolcase/base
v5.0.2
Published
Zero-dep JavaScript helpers and data structures for Node.js and the browser — events, async, collections, math, and more.
Maintainers
Readme
@toolcase/base
🧬 A toolbox of TypeScript helper functions and data structures for everyday application code. Zero runtime dependencies, isomorphic (Node.js + browser), tree-shakeable, dual ESM + CJS.
Why
Stop installing lodash, nanoid, p-retry, mitt, lru-cache, and a custom EventEmitter for every project. @toolcase/base bundles the small, common building blocks you keep re-implementing — typed, tested, no peer chain.
Install
npm install @toolcase/baseImport
import { Cache, retry, generateId } from '@toolcase/base' // browser + Node
import { Packing } from '@toolcase/base' // sprite/atlas packingAPI
Data structures
Cache<T>(fetchFn?, ms?, maxEntries?)
In-memory cache with a TTL, least-recently-used eviction and request coalescing. Use it for memoising expensive calls (DB lookups, API responses, parsed config) without pulling in a full LRU library. Concurrent misses for one key share a single fetch; a rejected fetch is not cached.
import { Cache } from '@toolcase/base'
const users = new Cache<User>((id: string) => db.findUser(id), 60_000, 5_000) // 60s TTL, at most 5000 entries
await users.get('u1') // fetches once, then serves from memory
users.invalidate('u1') // drop one entry (and abandon a fetch still in progress)
users.clear() // drop everything
const sessions = new Cache<Session>(null, 30_000) // no fetchFn: a loader per call
await sessions.load(token, () => db.findSession(token))
sessions.peek(token) // Session | undefined, never fetches
sessions.has(token) // boolean
sessions.set(token, session) // store directly
sessions.size // stored entriesInvalidation is safe against in-flight fetches: after invalidate(key) or clear(), a fetch that started earlier still answers its own callers but is not stored, and the next read starts a fresh fetch. That makes it safe to invalidate from a change notification (e.g. Postgres LISTEN/NOTIFY).
PriorityQueue<T>(compareFn)
Ordered queue that dequeues the smallest item according to your comparator. Useful for job schedulers, A* openSet, or any "do the most important thing first" loop.
import { PriorityQueue } from '@toolcase/base'
const jobs = new PriorityQueue<Job>((a, b) => a.priority - b.priority)
jobs.enqueue({ priority: 5, name: 'flush' })
jobs.enqueue({ priority: 1, name: 'log' })
jobs.dequeue() // { priority: 1, name: 'log' }ObjectPool<T>(factory, reset, initialSize)
Reuse instances instead of allocating. Handy in hot loops (game frame ticks, particle systems, websocket packet handlers) to keep GC pauses down.
import { ObjectPool } from '@toolcase/base'
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
p => { p.x = 0; p.y = 0 },
10
)
const v = pool.get()
v.x = 5
// … use it …
pool.release(v)State<T>(initialData)
Plain observable state object — emits change events on property updates. Lighter than Zustand or MobX when all you need is "tell me when this changed".
import { State } from '@toolcase/base'
const state = new State({ score: 0, lives: 3 })
state.on('change', (key, value) => console.log(`${key} → ${value}`))
state.set({ score: 10 })AdjacencyMatrix
Directed/undirected weighted graph. Use it for navmesh prototypes, dependency graphs, or any "what connects to what" model.
import { AdjacencyMatrix } from '@toolcase/base'
const g = new AdjacencyMatrix()
g.addVertex('A')
g.addVertex('B')
g.addEdge('A', 'B', 1)
g.getNeighbors('A')VectorClock
Vector clock for distributed system causality. Useful for CRDT-style sync, multiplayer ordering, or distributed tracing.
import { VectorClock } from '@toolcase/base'
const clock = new VectorClock('node-1')
clock.increment()
clock.merge(otherClock)WeightedRandom<T>
Random selection biased by per-item weights. Great for loot tables, A/B variant pickers, weighted retry strategies.
import { WeightedRandom } from '@toolcase/base'
const drops = new WeightedRandom<string>()
drops.add('common', 70)
drops.add('rare', 25)
drops.add('legendary', 5)
drops.pick() // 'common' | 'rare' | 'legendary'Events
EventEmitter
Minimal typed event emitter (no Node events shim, no peer deps). Drop-in for client and server.
import { EventEmitter } from '@toolcase/base'
const bus = new EventEmitter<{ ready: [string]; error: [Error] }>()
bus.on('ready', (msg) => console.log(msg))
bus.emit('ready', 'hello')Broadcast
Base class — extend it to give any class pub/sub without composition boilerplate.
import { Broadcast } from '@toolcase/base'
class FileWatcher extends Broadcast<{ change: [string] }> {
notify(path: string) { this.emit('change', path) }
}Async
retry<T>(fn, options?): Promise<T>
Retry an async function with backoff. For API calls, transient DB errors, etc.
import { retry } from '@toolcase/base'
const controller = new AbortController()
const data = await retry(({ attempt, signal }) => fetch('/api', { signal }).then(r => r.json()), {
retries: 3, // extra attempts after the first
minTimeout: 250, // first backoff; doubles each time (factor: 2)
shouldRetry: (error, attempt) => !(error instanceof TypeError), // false → rethrow now
signal: controller.signal // abort stops the backoff wait and rejects with signal.reason
})The callback receives { attempt, signal } (attempt is 1-based); zero-arg callbacks keep working. After an abort no further attempt starts.
Async.withTimeout(fn, ms, { signal? })
Rejects with timed out after <ms>ms at the deadline and aborts the AbortSignal passed to fn(signal) with that error, so the work can stop too. An outer signal is linked: aborting it aborts the work and rejects with its reason.
const user = await Async.withTimeout(signal => fetch('/api/user', { signal }).then(r => r.json()), 3_000)Async.Semaphore / Async.Mutex — forced acquire
acquire({ force: true }) / run(fn, { force: true }) takes a permit even when none is free (available may go negative), for nested work that already runs under the lock. Each forced acquire is paid back by its release() before queued waiters are woken.
Utility functions
| Function | Description |
|----------|-------------|
| generateId(length?) | Cryptographically random hex ID. Default 8 chars. |
| getNumberInRange(value, default?, min?, max?) | Parse + clamp a number. |
| toHex(value) | Number → hex string. |
| bufferToHex(buffer) | Uint8Array → hex string. |
| hexToBuffer(hex) | Hex string → Uint8Array. |
| formatByteSize(bytes) | 1536 → "1.5 KB". |
| deepEqual(a, b, { ignoreUndefined? }) | Structural equality (the one diff uses): key order ignored, arrays positional, Date by time value, NaN equals NaN. ignoreUndefined treats { a: undefined } like {} (JSON semantics). |
import { generateId, formatByteSize, retry } from '@toolcase/base'
generateId() // '3a7f2b1c'
generateId(16) // '3a7f2b1c9e4d8f01'
formatByteSize(1024 * 1024 * 3) // '3.0 MB'Validation
JSONSchema
Schema-based JSON validation without Ajv-sized footprint. Define field rules, validate, get back errors.
import { JSONSchema } from '@toolcase/base'
const userSchema = new JSONSchema()
userSchema.define('email', { type: 'string', required: true })
userSchema.define('age', { type: 'number', required: false })
const result = userSchema.validate({ email: '[email protected]', age: 30 })HTTP helpers
HTTP.Status, HTTP.RESTError, HTTP.RESTResponse — small primitives for building consistent REST responses across services.
import { HTTP } from '@toolcase/base'
throw new HTTP.RESTError(HTTP.Status.NOT_FOUND, 'user not found')
return new HTTP.RESTResponse(HTTP.Status.OK, { user })Sprite / rectangle packing
Packing — a full 2D rectangle packing toolkit (Skyline, MaxRects, Guillotine, Shelf, BinaryTree, multi-page planner, POT ceiling). Useful for building texture atlases, level layout, document layout, or any bin-packing problem.
import { Packing } from '@toolcase/base'
const result = Packing.Packer.pack(sprites, { algorithm: 'maxrects', pot: 'ceil' })
result.pages.forEach(page => {
// page.width, page.height, page.placed[]
})Procedural / visual helpers
LSystem— Lindenmayer system iterator for procedural generation (plants, fractals, dungeons).Color— Material design color palette constants.
Common patterns
Memoised fetch with retry:
import { Cache, retry } from '@toolcase/base'
const cache = new Cache<Profile>(null, 30_000)
const getProfile = (id: string) =>
cache.load(id, () => retry(() => api.profile(id)))Event-driven domain object:
import { Broadcast } from '@toolcase/base'
class Cart extends Broadcast<{ added: [Item]; removed: [Item] }> {
add(item: Item) { /* … */ this.emit('added', item) }
}Hot-loop pooling:
const bullets = new ObjectPool(makeBullet, resetBullet, 256)
function fire() {
const b = bullets.get()
// … update / render …
if (b.dead) bullets.release(b)
}