Public: anyone can read this; the crew can change it.

A ration a machine can read: src/ration.ts, a bucket per key and a retry that is true #31

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`src/ration.ts` is a new, self-contained token bucket: `take(key, now?, limit?)` returns `{ ok: true, remaining }` or `{ ok: false, retryAfterMs }`, with the clock as a parameter, no timers, no `Request`, no database — keys are strings and the answer is milliseconds, so rounding up for a `retry-after` header stays the door's job.

shipped production is serving it deploy 460b31c

opened this

src/ration.ts is a new, self-contained token bucket: take(key, now?, limit?) returns { ok: true, remaining } or { ok: false, retryAfterMs }, with the clock as a parameter, no timers, no Request, no database — keys are strings and the answer is milliseconds, so rounding up for a retry-after header stays the door's job.

The bucket is stored as one number, the time the schedule has run up to, rather than a tokens-and-timestamp pair. It is the same arithmetic (the header comment gives the formula and the equivalence), with the property the issue asks for: a refusal stores nothing, so the retry is measured against the very number it will later be compared to. src/ration.test.ts takes a refusal, advances by exactly retryAfterMs and succeeds, advances one millisecond less and fails — exactly, not within a tolerance. The other tests are hand calculations: twenty tokens counted down to zero then refused at 500 ms, 1.5 s of quiet worth three tokens, half-second refills accumulating (250, 125, 1) instead of truncating, a day of idling still holding only twenty, and one key never spending another's.

Two limits live here and nowhere else: PERSON_WRITES (20 burst, 2/s — what allow() applies today) and APP_EVENTS (100 burst, 5/s), each with a sentence on why those numbers. A test asserts PERSON_WRITES accepts exactly what allow() accepts, so #8's swap is a no-op in behaviour. Memory is capped at 10,000 keys by insertion-order LRU; the last test floods past the cap twice, asserts keyCount() stops at 10,000, and asserts an evicted key returns with a full bucket.

What I chose not to do: nothing calls it. src/http.ts is untouched and allow() still rations every door, so this pull adds a file and a test and changes nothing that runs. No README change either — there is no behaviour yet to document. No dependencies. The header comment says the ration is in memory, resets on restart and counts per process, and why that beats a SQLite write per request here. Worth a look: the perToken/ahead derivation in take, and the 1e-9 in remaining, which is there only so the last bit of a float divide cannot hide a whole token a caller holds.

🤖 Generated with Claude Code

Closes #24

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs