// zig fmt: off const std = @import("std"); const RateLimit = @This(); const assert = std.debug.assert; pub const Category = enum { account_creation, token, data, actions }; pub const Timespan = struct { counter: u32 = 0, limit: u32, timer_ms: u64 = 0, }; pub const CategoryArray = std.EnumArray(Category, RateLimit); seconds: ?Timespan = null, minutes: ?Timespan = null, hours: ?Timespan = null, last_update_at_ms: i64 = 0, pub fn init(now_ms: i64, limit_per_hour: ?u32, limit_per_minute: ?u32, limit_per_second: ?u32) RateLimit { var seconds: ?Timespan = null; if (limit_per_second) |limit| { seconds = Timespan{ .limit = limit }; } var minutes: ?Timespan = null; if (limit_per_minute) |limit| { minutes = Timespan{ .limit = limit }; } var hours: ?Timespan = null; if (limit_per_hour) |limit| { hours = Timespan{ .limit = limit }; } return RateLimit{ .seconds = seconds, .minutes = minutes, .hours = hours, .last_update_at_ms = now_ms }; } pub fn update_timers(self: *RateLimit, now_ms: i64) void { const time_passed_ms = now_ms - self.last_update_at_ms; assert(time_passed_ms >= 0); inline for (.{ .{ &self.seconds, std.time.ms_per_s }, .{ &self.minutes, std.time.ms_per_min }, .{ &self.hours, std.time.ms_per_hour }, }) |tuple| { const maybe_timespan = tuple[0]; const timespan_size = tuple[1]; if (maybe_timespan.*) |*timespan| { timespan.timer_ms += @intCast(time_passed_ms); const ms_per_request = @divFloor(timespan_size, timespan.limit); const requests_passed: u32 = @intCast(@divFloor(timespan.timer_ms, ms_per_request)); timespan.counter -= @min(timespan.counter, requests_passed); timespan.timer_ms = @mod(timespan.timer_ms, ms_per_request); } } self.last_update_at_ms = now_ms; } pub fn increment_counters(self: *RateLimit) void { inline for (.{ &self.hours, &self.minutes, &self.seconds, }) |maybe_timespan| { if (maybe_timespan.*) |*timespan| { timespan.counter += 1; } } }