- stored/replayed headers widen from content-type only to an allowlist (content-type, location, etag, cache-control), matched case-insensitively -- a redirect() lost its Location on its own first response, not just replay - add pool_slots(Pool) -> multi PoolSlot and pool_of(multi PoolSlot) -> Pool - Pool is demand-promoted to traced (WO-E222) and can't live in actor state; PoolSlot/multi PoolSlot never is, the same shape chat/main.wo's Room already holds directly -- this is what lets an app actually shard across N actors per connection instead of a forced one-slot pool - log a genuine pool_select trap instead of silently folding it into 503 - fix stale comments: the prune below IS a delete-then-insert (of a fresh row, not the same one) contradicting the doc comment above it; the catch shape referenced in two comments had changed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit b738269314f01a95dee1341437c7661ce9e28730)
26 lines
No EOL
928 B
Text
26 lines
No EOL
928 B
Text
-- porch/middleware/store.wo — store-backed middleware tables.
|
|
-- Two purpose-shaped @table classes for rate limiting and idempotency.
|
|
-- Iteration 1 of the porch track.
|
|
|
|
-- Rate limiter: fixed-window counter.
|
|
-- Key format: "ip:192.168.1.1" or "principal:alice"
|
|
-- Window = start of current window in time.ticks (µs monotonic)
|
|
@table(name: "rate_limit_counters", index: [key])
|
|
class RateLimitCounter {
|
|
key: Text @unique
|
|
count: Int
|
|
window: Int
|
|
}
|
|
|
|
-- Idempotency: stored response for replay.
|
|
-- Key format: "idem:keyheader" or "idem:keyheader:sha256(method|path|body)"
|
|
-- Response = JSON-encoded Resp {status, headers, body} (headers allowlist:
|
|
-- content-type, location, etag, cache-control)
|
|
-- created_at = time.ticks when stored (µs monotonic) for lazy expiry
|
|
@table(name: "idempotency_keys", index: [key])
|
|
class IdempotencyKey {
|
|
key: Text @unique
|
|
response: Text
|
|
created_at: Int
|
|
digest: Text
|
|
} |