Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions beamsync/rate_limiter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ func TestClientRateLimiterEvictsOldestClientWhenFull(t *testing.T) {
}
}

func TestClientRateLimiterPrunesExpiredClients(t *testing.T) {
now := time.Date(2026, 5, 28, 0, 0, 0, 0, time.UTC)
limiter := newClientRateLimiter(2, time.Minute)
limiter.now = func() time.Time { return now }

limiter.clients["10.0.0.1"] = &rateLimitState{
windowStart: now.Add(-3 * time.Minute),
count: 1,
lastSeen: now.Add(-3 * time.Minute),
}
limiter.clients["10.0.0.2"] = &rateLimitState{
windowStart: now.Add(-30 * time.Second),
count: 1,
lastSeen: now.Add(-30 * time.Second),
}

limiter.prune(now)

if _, ok := limiter.clients["10.0.0.1"]; ok {
t.Fatal("stale client should be pruned")
}
if _, ok := limiter.clients["10.0.0.2"]; !ok {
t.Fatal("recent client should remain")
}
}

func TestRateLimitMiddlewareReturns429(t *testing.T) {
now := time.Date(2026, 5, 28, 0, 0, 0, 0, time.UTC)
limiter := newClientRateLimiter(1, time.Minute)
Expand Down
48 changes: 45 additions & 3 deletions beamsync/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,58 @@ type clientRateLimiter struct {
now func() time.Time
}

const clientRateLimiterPruneInterval = 2 * time.Minute

var clientRateLimiterPruner = struct {
once sync.Once
mu sync.Mutex
limiters map[*clientRateLimiter]struct{}
}{
limiters: make(map[*clientRateLimiter]struct{}),
}

func newClientRateLimiter(limit int, window time.Duration) *clientRateLimiter {
return &clientRateLimiter{
limiter := &clientRateLimiter{
limit: limit,
window: window,
maxClients: 4096,
clients: make(map[string]*rateLimitState),
now: time.Now,
}
registerClientRateLimiter(limiter)
return limiter
}

func registerClientRateLimiter(limiter *clientRateLimiter) {
if limiter == nil {
return
}

clientRateLimiterPruner.mu.Lock()
clientRateLimiterPruner.limiters[limiter] = struct{}{}
clientRateLimiterPruner.mu.Unlock()

clientRateLimiterPruner.once.Do(func() {
go runClientRateLimiterPruner()
})
}

func runClientRateLimiterPruner() {
ticker := time.NewTicker(clientRateLimiterPruneInterval)
defer ticker.Stop()

for now := range ticker.C {
clientRateLimiterPruner.mu.Lock()
limiters := make([]*clientRateLimiter, 0, len(clientRateLimiterPruner.limiters))
for limiter := range clientRateLimiterPruner.limiters {
limiters = append(limiters, limiter)
}
clientRateLimiterPruner.mu.Unlock()

for _, limiter := range limiters {
limiter.prune(now)
}
}
}

func (l *clientRateLimiter) allow(client string) rateLimitDecision {
Expand All @@ -178,8 +222,6 @@ func (l *clientRateLimiter) allow(client string) rateLimitDecision {
l.mu.Lock()
defer l.mu.Unlock()

l.prune(now)

state, ok := l.clients[client]
if !ok || now.Sub(state.windowStart) >= l.window {
if !ok {
Expand Down
34 changes: 22 additions & 12 deletions desktop/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
let transferStatsNow = Date.now();
let transferStatsTimer;
let transferStatsThrottleTimer;
let qrGenerationTimer = null;
let pendingTransferStats = null;
let lastTransferStatsUpdateAt = 0;
let transferSpeeds = {
Expand Down Expand Up @@ -482,6 +483,7 @@
clearTimeout(batchTimer);
clearTimeout(_progressTimeout);
clearTimeout(transferStatsThrottleTimer);
clearTimeout(qrGenerationTimer);
clearInterval(transferStatsTimer);
});

Expand Down Expand Up @@ -550,18 +552,26 @@
}

function generateQR(text) {
if (!text) return;
QRCode.toDataURL(
text,
{
width: 220,
margin: 2,
color: { dark: "#0A0A0A", light: "#00000000" },
},
(err, url) => {
if (!err) qrImage = url;
},
);
clearTimeout(qrGenerationTimer);

if (!text) {
qrImage = "";
return;
}

qrGenerationTimer = setTimeout(() => {
QRCode.toDataURL(
text,
{
width: 220,
margin: 2,
color: { dark: "#0A0A0A", light: "#00000000" },
},
(err, url) => {
if (!err) qrImage = url;
},
);
}, 100);
}

function playSound(type) {
Expand Down
Loading