Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

Rate limiting

A cache-backed rate limiter — the desktop counterpart to Laravel's RateLimiter. Count attempts against a key that expires after a decay window, and throttle once the limit is hit. Needs the Cache (CacheProvider).

use elyra::{command, Ctx, Cache};
use std::time::Duration;

#[command]
async fn login(ctx: Ctx, email: String, password: String) -> Result<Session, String> {
    let limiter = ctx.get::<Cache>().limiter();
    let key = format!("login:{email}");

    if limiter.too_many_attempts(&key, 5) {
        return Err("Too many attempts. Try again later.".into());
    }

    match authenticate(&email, &password) {
        Some(session) => { limiter.clear(&key); Ok(session) }
        None => {
            limiter.hit(&key, Duration::from_secs(60)); // 5 tries / minute
            Err("Invalid credentials.".into())
        }
    }
}

API

Method Does
too_many_attempts(key, max) whether key reached max
hit(key, decay) record an attempt; returns the new count
attempts(key) attempts so far
remaining(key, max) attempts left (never negative)
clear(key) reset the counter
attempt(key, max, decay, callback) run callback if under max (recording a hit), else None

attempt is the concise form:

let limiter = ctx.get::<Cache>().limiter();
match limiter.attempt("send-email", 3, Duration::from_secs(60), || send()) {
    Some(result) => result,
    None => return Err("rate limited".into()),
}

The counter is a cache entry that expires decay after its first hit, so limits reset automatically. In-process (per app run), like the cache itself.

attempt() does the check and the increment under one cache lock (Cache::increment_if_below), so concurrent callers can't both see "under the limit" and push the counter past max.

Related