Guides on this site are the repository READMEs; the API reference is generated from the sources. AI agents: npx skills add mirek/prelude or see the prelude skill.
prelude monorepo
A pnpm workspace containing small, focused TypeScript utility packages published under the @prelude/* scope.
What this repository contains
This repository is organized as a monorepo. Every directory under packages/* is a workspace package with its own package.json; pnpm manifests:check fails on a directory without one, because the build, typecheck and pack scripts discover packages by manifest and would otherwise skip it silently.
- Shared workspace configuration at the root (
pnpm-workspace.yaml, rootpackage.json). - Individual package source, tests, and package-level docs in each
packages/<name>directory.
Quick start
Prerequisites
- Node.js 22 or later (see Supported runtimes)
- pnpm
11.1.3through the rootpackageManagerdeclaration
Install dependencies
pnpm install --frozen-lockfile
Run the CI-equivalent quality gate
pnpm verify
pnpm verify runs the same checks as GitHub Actions, in this order:
pnpm manifests:check— verifies normalized package manifests, exports, and workspace TypeScript configs.pnpm release-hooks:check— rejects package-local version, push, or publish hooks.pnpm makefiles:check— rejects package recipes that build withtscdirectly or use unquoted recursive globs.pnpm docs:check— verifies package READMEs, license references, manifest descriptions and the package index below.pnpm test:check— rejects unseeded randomness and unsupportednode:testtimeout arguments in the default suite.pnpm lint— type-aware linting acrosspackages.pnpm typecheck— checks every package library and test TypeScript project independently.pnpm test— runs all colocated*.test.tsfiles through Node's test runner.pnpm test:scripts— runs the tests of the repository scripts themselves.pnpm build— rebuilds every package with atsconfig.lib.jsonfrom clean source.pnpm pack:check— packs every public package and installs the tarballs into an isolated consumer project.
Release a change
Every pull request adds a changeset (pnpm changeset, or pnpm changeset --empty when nothing publishable changed); CI rejects pull requests without one. Merged changesets accumulate in an automated chore(release): version packages pull request; merging it bumps versions and changelogs, and the Publish workflow then publishes every not-yet-published version to npm with provenance and tags it <package>@<version>. See .changeset/README.md.
Concurrency tests use explicit barriers, fake schedulers, and awaited task settlement rather than random sleeps or eventual polling. Optional randomized stress coverage must use a reproducible seed.
The data structures with the largest state spaces (rb-tree and its Bag/Map, radix-trie, range-set, sorted-array, set, channel, serial-queue) also have model-based property tests (packages/*/src/model.test.ts, laws.test.ts) built on the private @prelude/testing package: checkTrace generates operation traces from a seed, replays them against the structure and a naive reference model with every invariant checked after each step, and on failure shrinks the trace and reports seed, trial and the minimal operation sequence. The default suite runs a bounded number of trials; SLOW_TESTS=<factor> multiplies them, which the weekly Stress workflow (.github/workflows/stress.yml, also runnable by hand) does with a factor of 20.
The package check verifies declared entry points, declaration resolution, root and subpath runtime imports, workspace dependency rewriting, the externally published TypeScript configs, and exclusion of development-only files. Isomorphic packages are additionally imported with Node's platform globals removed and every Node builtin unresolvable, and their declarations are type-checked without @types/node. Every publishable library also builds from source during its own prepack lifecycle.
GitHub Actions runs these checks independently on Node.js 22 (the supported floor), 24 and 26. The aggregate CI job is suitable as the required branch-protection check.
Supported runtimes
Every published @prelude/* package declares "engines": { "node": ">=22" }; the root manifest declares the same. Node 22 is the oldest release line still in maintenance, and CI runs the full quality gate on it and on the current release lines, so the floor is exercised, not inferred.
- Compile target.
@prelude/tsconfig/base.jsonsetstarget: ES2024andlib: ["ES2024", "ES2025.Iterator", "ES2025.Collection"]: the ES2024 language and library plus the ES2025 iterator helpers andSetmethods, all of which Node 22 ships.Symbol.dispose/Symbol.asyncDispose(also in Node 22) are declared byisomorphic.d.tsrather than by theESNext.Disposablelib, because that lib would also exposeDisposableStack,AsyncDisposableStackandSuppressedError, which are Node 24+. Backend and test projects see those (andFloat16Array) anyway through@types/node's own lib references, so.oxlintrc.jsonbans them withno-restricted-globals. Nothing newer (Promise.try,RegExp.escape, ...) is visible to the compiler, so it cannot enter a package by accident. Widening the list is a support-policy change: it requires the feature to be available in the oldest supported Node and an entry in this section. - Node types. The workspace type-checks against
@types/nodefor the oldest supported release, so backend packages cannot use a Node API added after the floor. - Package classes. A package is isomorphic when its
tsconfig.lib.jsonextends@prelude/tsconfig/isomorphic.json(compiled withtypes: [], so no Node global ornode:module resolves) or Node-only when it extendsbackend.json. Currently Node-only:@prelude/assert,@prelude/fs,@prelude/log,@prelude/progress,@prelude/rb-tree,@prelude/refute,@prelude/repl(and the private@prelude/json); every other package is isomorphic and is expected to run in browsers, workers and edge runtimes that support ES2024.pnpm pack:checkenforces both classes on every CI Node version. - TypeScript.
@prelude/tsconfigdeclarestypescript >=6as a peer dependency: itslibentries use theES2025.*names TypeScript 6 introduced (5.x only knows them asESNext.*). Package declarations are emitted and consumer-checked (pack:check) with the workspace TypeScript version; older compilers are not tested. - Dropping a runtime. Raising the floor is a breaking change for every package: bump the
enginesconstant inscripts/normalize-package-manifests.mjs, the CI matrix,@types/nodeand this section together, and release the affected packages with a major bump.
Clean TypeScript build artifacts
make clean
Cancellation
Long-lived asynchronous operations across the packages share one cancellation convention, built on the platform's AbortSignal:
- An operation that can wait accepts an optional
signal(usually in a trailing options object:sleep(ms, { signal }),channel.read({ signal }),map(f, { concurrency, signal }),select({ signal }, ...attempts)). - A signal that is already aborted makes the operation reject (or the generator throw) at once, before it touches anything.
- Aborting a pending operation removes its listeners, clears its timers, withdraws its pending reads/writes/queue entries and rejects it with
signal.reason— the same value everywhere; no package wraps or replaces it.AbortController.abort()without a reason yields the platform'sAbortErrorDOMException. - Once an operation has settled it detaches from the signal, so aborting later is a no-op and never produces an unhandled rejection.
- Work that is already in flight in user code (a mapping function, a retry attempt, a queue task,
fintimeout) is not interrupted — JavaScript cannot interrupt it — but the operation does not wait for it: it rejects at once and the work's eventual result is dropped (a late rejection is swallowed, never left unhandled). Pass the same signal into that work when it should stop too.consumeis the exception: it awaits callbacks already in flight before rejecting, as it does on failure. - Consumers that want a graceful stop rather than an error use the iterator protocol (
breakout offor await,return()), which closes upstream resources; a signal is for stopping from the outside.
Packages following the convention: function (sleep, timeout, eventually, throttle), emitter (eventually, eventuallyIf), channel (read, maybeRead, write, maybeWrite, writeIgnore, select/selectNext/selectAsync, after, ofIterable, ofAsyncIterable), serial-queue (pushWith), progress (start, also disposable), remote-clock (midSeconds, midSecondsInterval), async-generator (sleep, jitter, ofInterval, map, tap, consume), plus actor/remote-actor (ask) and jsonrpc (call) which already did. Operations without a signal are either instantaneous or already have an explicit stop (Emitter.on returns an unregister function, throttle returns a function whose pending call the signal drops, serial-queue.rejectAll empties a queue).
Package index
Generated from the workspace manifests by pnpm docs:write; pnpm docs:check fails when it drifts.
| Directory | Package | Description |
|---|---|---|
packages/actor |
@prelude/actor |
Actor module: stateful message processors with ask/reply, bounded mailboxes, failure directives and supervision hooks. |
packages/array |
@prelude/array |
Array utilities: safe indexing, sampling and shuffling, sorting and searching, grouping, swap-delete and vector helpers. |
packages/assert |
@prelude/assert |
Composable runtime assertions that narrow unknown values to typed ones and throw AssertionError with the failing path; built on @prelude/validation. |
packages/async-generator |
@prelude/async-generator |
Composable async iterable transforms: map, filter, batch, window, buffered and concurrent processing with backpressure. |
packages/channel |
@prelude/channel |
Go-style channels for async code: buffered or unbuffered, async-iterable, with select over reads and writes. |
packages/cmp |
@prelude/cmp |
Sound comparators: strict -1 |
packages/emitter |
@prelude/emitter |
Type-safe event emitter with once, predicate-filtered listeners and promise-based waits with timeouts. |
packages/eq |
@prelude/eq |
Structural equality combinators for primitives, arrays, tuples, records and partial objects. |
packages/err |
@prelude/err |
Errors with a severity and a code, plus helpers to create, inspect and rethrow them consistently. |
packages/fs |
@prelude/fs |
Node.js file-system helpers: JSON and string read/write, existence checks and deterministic depth-first traversal. |
packages/function |
@prelude/function |
Function utilities: pipe, memoize, throttle, timeout, retry (eventually), sleep, serial execution and logic combinators. |
packages/generator |
@prelude/generator |
Iterable and generator utilities: lazy map, filter, flatMap, batch, group, permutations, primes and other transforms over sync iterables. |
packages/json |
@prelude/json (private) |
JSON encoding and decoding of non-JSON-native values (Set, Map, Date, RegExp, bigint, errors) through tagged coders. |
packages/jsonrpc |
@prelude/jsonrpc |
JSON-RPC 2.0 client and request handling over any message transport, with request timeouts, abort signals and typed payloads. |
packages/log |
@prelude/log |
Lightweight logger with severity levels, namespaces, pluggable targets and environment-variable level control. |
packages/parser |
@prelude/parser |
Parser combinators over string readers with location-aware failures, plus an RFC 8259 JSON grammar. |
packages/predicate |
@prelude/predicate |
Type-guard combinators for primitives, objects, arrays, tuples, records and unions; built on @prelude/validation. |
packages/prelude |
@prelude/prelude |
Low-level primitives shared by @prelude/* packages: pipe and common utility types. |
packages/progress |
@prelude/progress |
Terminal progress display for Node.js: multi-worker progress bars, spinners and percentages on stdout. |
packages/radix-trie |
@prelude/radix-trie |
Radix (compressed prefix) trie for string sets: insert, membership and shortest/longest prefix matching. |
packages/range-set |
@prelude/range-set |
Sets of value-carrying ranges over ordered keys with union, intersection and difference under pluggable value merges. |
packages/rb-tree |
@prelude/rb-tree |
Persistent red-black tree with duplicate counts, range counting and Bag/Map wrappers, with exported invariant checks. |
packages/refute |
@prelude/refute |
Validators that return ok/refuted results with a reason instead of throwing, with predicate and assertion interpreters; built on @prelude/validation. |
packages/remote-actor |
@prelude/remote-actor |
Remote actor module: send/ask to an actor over any message transport (MessagePort, workers, sockets). |
packages/remote-clock |
@prelude/remote-clock |
Clock synchronisation against a remote time source: offset estimation, remote now/date and mid-second ticks. |
packages/repl |
@prelude/repl |
Node.js REPL helpers: run code snippets extracted from Markdown in a sandbox with builtin modules and globals. |
packages/semver |
@prelude/semver |
Semantic Versioning 2.0 parsing, comparison and precedence ordering. |
packages/serial-queue |
@prelude/serial-queue |
Promise-returning serial task queue: pushed work runs one at a time, in order, with bulk rejection. |
packages/set |
@prelude/set |
Set helpers over the native Set: union, intersection, difference, equality, numeric ranges and sorted/shuffled views. |
packages/sorted-array |
@prelude/sorted-array |
Arrays kept sorted by a comparator with binary search, insert, insert-ignore and upsert. |
packages/string |
@prelude/string |
String utilities: blank checks, case conversion, indentation, truncation, line operations, search-replace and edit distance. |
packages/supervisor |
@prelude/supervisor |
Supervisor module: restart strategies (one-for-one, all-for-one, rest-for-one) and restart limits for @prelude/actor. |
packages/testing |
@prelude/testing (private) |
Workspace-internal test helpers: seeded pseudo-random generation and trace-based model checking with shrinking. |
packages/tsconfig |
@prelude/tsconfig |
Shared TypeScript configurations for the prelude monorepo. |
packages/validation |
@prelude/validation |
Shared validator core behind @prelude/assert, @prelude/predicate and @prelude/refute: one implementation of the primitive, container and combinator checks producing structured, path-carrying failures. |
packages/wait-group |
@prelude/wait-group |
Go-style WaitGroup: count outstanding work and await completion, with invalid-counter protection. |
packages/xml |
@prelude/xml |
XML 1.0 parser with namespace processing, precise error locations and JSON conversion helpers; no DTD processing or external resources. |
Package documentation
Every package ships Readme.md and License.md (CC0-1.0). pnpm docs:check (part of pnpm verify) enforces the shared shape:
- a top-level heading and a specific
descriptioninpackage.json(noFoo module.placeholders — the description is what npm and the index above show); - an install command (
npm i -E @prelude/<name>) and an import example; - a
# Licensesection that points at./License.md; - no machine-local paths, badges for retired services, links to the retired standalone repositories, or license text that contradicts the manifest;
- relative links that resolve.
A minimal package README therefore looks like:
# Foo module
One or two sentences on what the package is for and when to reach for it.
# Usage
npm i -E @prelude/foo
import * as Foo from '@prelude/foo'
Foo.bar(1)
# License
This package is dedicated to the public domain under [CC0 1.0](./License.md).
Package manifests get repository, homepage, bugs, files, exports and types from pnpm manifests:write, derived from the package directory, so they cannot drift from the repository layout.
Documentation site and AI agents
The documentation site at https://mirekrusin.com/prelude/ is built by pnpm site:build (scripts/build-site.mjs) from the Markdown already in this repository — this README, every packages/*/Readme.md and CHANGELOG.md — plus a TypeDoc API reference under /api/, and is deployed by the Pages workflow on every push to main. Nothing on the site is written twice: package pages are the package READMEs.
For language models the site also publishes:
/llms.txt— an index of the guides, the API reference and the skill;/llms-full.txtis every guide in one file./.well-known/agent-skills/index.json— the agent-skills discovery index, with the SHA-256 digest of the skill file./.well-known/agent-skills/prelude/SKILL.md— thepreludeskill: which@prelude/*package to reach for, how they compose, conventions and common mistakes. Its source isskills/prelude/SKILL.md(the workspace's.claude/skills/preludelinks to it) and the build copies it verbatim.
Install the skill into an agent (Claude Code, Codex, Cursor and others supported by skills):
npx skills add mirek/prelude
The well-known index is published under the site's base path (/prelude/.well-known/agent-skills/index.json); origin-root discovery (npx skills add https://<domain>) needs the site on its own domain — set SITE_URL accordingly in the Pages workflow, nothing else changes.
pnpm site:preview builds the site without the API reference (a few seconds instead of a minute) into site/, which is ignored by git.
License
The repository root is licensed under CC0-1.0. Individual packages may declare their own licenses in package-level files/manifests.