perf: throttle replay-cache pruning to once per second

ober

fc98c0e640f805092cba278cdd70dc08b7d15757

diff --git a/edge.ss b/edge.ss
index a5017ae..33c7fac 100644
--- a/edge.ss
+++ b/edge.ss
@@ -692,6 +692,17 @@
       *replay-keys*)
     (for-each (lambda (key) (hash-remove! *replay-keys* key)) expired)))
 
+;; Pruning scans the whole replay cache, so doing it on every claim made
+;; webhook throughput O(n) per request (O(n^2) at rate) under the mutex.
+;; Throttle it to at most once per second; expired keys linger briefly, which
+;; only nudges the capacity check. Caller holds *replay-mutex*.
+(def *last-replay-prune-second* 0)
+
+(def (%maybe-prune-replay-keys! now)
+  (when (>= (- now *last-replay-prune-second*) 1)
+    (set! *last-replay-prune-second* now)
+    (%prune-replay-keys! now)))
+
 (def (claim-replay-key! source event-id nonce timestamp)
   (let* ([now (time-second (current-time))]
          [expires (+ now *replay-window-seconds*)]
@@ -704,7 +715,7 @@
                         (string->utf8
                           (string-append "nonce\n" source "\n" nonce))))])
     (with-mutex *replay-mutex*
-      (%prune-replay-keys! now)
+      (%maybe-prune-replay-keys! now)
       (cond
         [(or (hash-key? *replay-keys* event-key)
              (hash-key? *replay-keys* nonce-key)) #f]