Development
Build valve, run its test suites and benchmarks, and work on these docs.
Set up
Use Node.js 24 and pnpm 11.
pnpm install
pnpm buildRun checks
pnpm checkpnpm check runs lint, typecheck, unit and property tests with coverage, type
inference tests, builds, source conventions, package validation, and bundle size
limits. Run them individually while working:
| Command | Runs |
|---|---|
pnpm test | Unit and property tests |
pnpm test:coverage | Unit tests with coverage |
pnpm test:types | Type inference tests |
pnpm lint | ESLint and Prettier |
pnpm check:size | Bundle size limits |
Test against Redis and Valkey
docker compose -f compose.test.yml up -d
REDIS_URL=redis://127.0.0.1:6379 pnpm test:integrationThe integration suite covers Lua parity with the memory store, ioredis and
node-redis clients, script-cache recovery, shared pauses, timeouts, server
restarts, post-timeout offline replay, and bounds across eight child processes.
The replay suite uses a local TCP proxy and a separate healthy observer. It
holds reconnection until after the store timeout, compares enabled/disabled
offline queues with ioredis 5 and node-redis 4/5, and checks all mutating
operations and wait/throw/open policies with ioredis. It checks server rate
state, lease timestamps and expiry, single-client pause/resume order, and
callback counts. These probes do not cover every network partition, cluster
failover, or cross-client control race. CI runs it against Redis
6.2, 7.4, and 8, and Valkey 7.2 and 8. Set REDIS_CLUSTER_URL to include
Redis Cluster checks.
Fault injection for state loss
The integration suite deletes valve's keys to check that a lost state starts a fresh budget. Server faults are manual probes. Run them only against a disposable instance:
docker run -d --rm --name valve-fault -p 16400:6379 redis:7.4 --save '' --appendonly noConnect the test client to redis://127.0.0.1:16400. Run Redis commands against
that instance with redis-cli -p 16400. Use a fixed-window rule
{ limit: 1, interval: '1m' }. Call store.acquire once, confirm a second call
returns granted: 0, inject one fault, then acquire again with the same prefix
and id. The no-eviction and failover probes use fresh ids as described below.
| Fault | Inject | Expected after the fault |
|---|---|---|
| Flush | redis-cli -p 16400 FLUSHDB | Granted 1 |
| Restart | docker restart valve-fault | Granted 1 |
| Eviction | CONFIG SET maxmemory-policy volatile-ttl, lower maxmemory below used_memory, write until evicted_keys rises, then set maxmemory back to 0 | Granted 1 |
| No eviction | CONFIG SET maxmemory-policy noeviction, lower maxmemory below used_memory, then acquire with a fresh id; inspect that id's rule key | store.acquire rejects with OOM; the new rule key stays absent |
| Failover | Start a replica and wait for it to sync, cut its replication link, acquire on the primary with a fresh id and confirm granted: 1, then run REPLICAOF NO ONE on the replica and acquire there with that id | Granted 1 |
The no-eviction row calls the store directly. Through valve, the OOM follows
the configured onStoreError behavior: throw rejects with STORE_UNAVAILABLE,
wait retries, and open admits jobs without shared rate rules. Restore
maxmemory to 0 before running another probe. For failover, target the replica
explicitly when promoting it and when making the final acquire.
Every row except "No eviction" shows silent loss. See Durability.
Benchmarks
pnpm bench # Local scheduler workloads
pnpm bench:compare # Repeated comparisons. Add REDIS_URL and VALKEY_URL for distributed runs.Recorded results and methodology live in packages/valve/bench/COMPARISON.md.
Work on the docs
The site is built with Next.js and Fumadocs. Pages are MDX files in
docs/content/docs/.
pnpm docs:dev # http://127.0.0.1:4321
pnpm docs:build # Static export to docs/out, including TypeDocBrowser tests run against the built site:
pnpm --filter @valve/web test:browserSee CONTRIBUTING.md
for commit conventions and changesets.