this repo has no description

🎨 Refactor the code

+127 -103
+7 -103
src/glimit.gleam
··· 35 35 //// ``` 36 36 //// 37 37 38 - import gleam/dict 39 38 import gleam/erlang/process.{type Subject} 40 - import gleam/list 41 39 import gleam/option.{type Option, None, Some} 42 - import gleam/otp/actor 43 - import gleam/result 44 - import glimit/utils 45 - 46 - /// The messages that the actor can receive. 47 - /// 48 - pub type Message(id) { 49 - /// Stop the actor. 50 - Shutdown 51 - 52 - /// Mark a hit for a given identifier. 53 - Hit(identifier: id, reply_with: Subject(Result(Nil, Nil))) 54 - } 40 + import glimit/actor 55 41 56 42 /// The rate limiter's public interface. 57 43 /// 58 44 pub type RateLimiter(a, b, id) { 59 45 RateLimiter( 60 - subject: Subject(Message(id)), 46 + subject: Subject(actor.Message(id)), 61 47 handler: fn(a) -> b, 62 48 identifier: fn(a) -> id, 63 49 ) 64 50 } 65 51 66 - /// A rate limiter. 52 + /// A builder for configuring the rate limiter. 67 53 /// 68 54 pub type RateLimiterBuilder(a, b, id) { 69 55 RateLimiterBuilder( ··· 75 61 ) 76 62 } 77 63 78 - /// The actor state. 79 - /// 80 - type State(a, b, id) { 81 - RateLimiterState( 82 - hit_log: dict.Dict(id, List(Int)), 83 - per_second: Option(Int), 84 - per_minute: Option(Int), 85 - per_hour: Option(Int), 86 - ) 87 - } 88 - 89 - fn handle_message( 90 - message: Message(id), 91 - state: State(a, b, id), 92 - ) -> actor.Next(Message(id), State(a, b, id)) { 93 - case message { 94 - Shutdown -> actor.Stop(process.Normal) 95 - Hit(identifier, client) -> { 96 - // Update hit log 97 - let timestamp = utils.now() 98 - let hits = 99 - state.hit_log 100 - |> dict.get(identifier) 101 - |> result.unwrap([]) 102 - |> list.filter(fn(hit) { hit >= timestamp - 60 * 60 }) 103 - |> list.append([timestamp]) 104 - let hit_log = 105 - state.hit_log 106 - |> dict.insert(identifier, hits) 107 - let state = RateLimiterState(..state, hit_log: hit_log) 108 - 109 - // Check rate limits 110 - // TODO: optimize into a single loop 111 - let hits_last_hour = hits |> list.length() 112 - 113 - let hits_last_minute = 114 - hits 115 - |> list.filter(fn(hit) { hit >= timestamp - 60 }) 116 - |> list.length() 117 - 118 - let hits_last_second = 119 - hits 120 - |> list.filter(fn(hit) { hit >= timestamp - 1 }) 121 - |> list.length() 122 - 123 - let limit_reached = { 124 - case state.per_hour { 125 - Some(limit) -> hits_last_hour > limit 126 - None -> False 127 - } 128 - || case state.per_minute { 129 - Some(limit) -> hits_last_minute > limit 130 - None -> False 131 - } 132 - || case state.per_second { 133 - Some(limit) -> hits_last_second > limit 134 - None -> False 135 - } 136 - } 137 - 138 - case limit_reached { 139 - True -> process.send(client, Error(Nil)) 140 - False -> process.send(client, Ok(Nil)) 141 - } 142 - 143 - actor.continue(state) 144 - } 145 - } 146 - } 147 - 148 64 /// Create a new rate limiter builder. 149 65 /// 150 66 pub fn new() -> RateLimiterBuilder(a, b, id) { ··· 208 124 /// function or handler function is missing. 209 125 /// 210 126 pub fn build(config: RateLimiterBuilder(a, b, id)) -> RateLimiter(a, b, id) { 211 - let state = 212 - RateLimiterState( 213 - hit_log: dict.new(), 214 - per_second: config.per_second, 215 - per_minute: config.per_minute, 216 - per_hour: config.per_hour, 217 - ) 218 - 219 127 RateLimiter( 220 - subject: case actor.start(state, handle_message) { 128 + subject: case 129 + actor.new(config.per_second, config.per_minute, config.per_hour) 130 + { 221 131 Ok(subject) -> subject 222 132 Error(_) -> panic as "Failed to start rate limiter actor" 223 133 }, ··· 237 147 pub fn apply(func: fn(a) -> b, limiter: RateLimiter(a, b, id)) -> fn(a) -> b { 238 148 fn(input: a) -> b { 239 149 let identifier = limiter.identifier(input) 240 - case actor.call(limiter.subject, Hit(identifier, _), 10) { 150 + case actor.hit(limiter.subject, identifier) { 241 151 Ok(Nil) -> func(input) 242 152 Error(Nil) -> limiter.handler(input) 243 153 } 244 154 } 245 155 } 246 - 247 - /// Stop the rate limiter agent. 248 - /// 249 - pub fn stop(limiter: RateLimiter(a, b, id)) { 250 - actor.send(limiter.subject, Shutdown) 251 - }
+120
src/glimit/actor.gleam
··· 1 + //// The rate limiter actor. 2 + //// 3 + 4 + import gleam/dict 5 + import gleam/erlang/process.{type Subject} 6 + import gleam/list 7 + import gleam/option.{type Option, None, Some} 8 + import gleam/otp/actor 9 + import gleam/result 10 + import glimit/utils 11 + 12 + /// The messages that the actor can receive. 13 + /// 14 + pub type Message(id) { 15 + /// Stop the actor. 16 + Shutdown 17 + 18 + /// Mark a hit for a given identifier. 19 + Hit(identifier: id, reply_with: Subject(Result(Nil, Nil))) 20 + } 21 + 22 + /// The actor state. 23 + /// 24 + type State(a, b, id) { 25 + RateLimiterState( 26 + hit_log: dict.Dict(id, List(Int)), 27 + per_second: Option(Int), 28 + per_minute: Option(Int), 29 + per_hour: Option(Int), 30 + ) 31 + } 32 + 33 + fn handle_message( 34 + message: Message(id), 35 + state: State(a, b, id), 36 + ) -> actor.Next(Message(id), State(a, b, id)) { 37 + case message { 38 + Shutdown -> actor.Stop(process.Normal) 39 + Hit(identifier, client) -> { 40 + // Update hit log 41 + let timestamp = utils.now() 42 + let hits = 43 + state.hit_log 44 + |> dict.get(identifier) 45 + |> result.unwrap([]) 46 + |> list.filter(fn(hit) { hit >= timestamp - 60 * 60 }) 47 + |> list.append([timestamp]) 48 + let hit_log = 49 + state.hit_log 50 + |> dict.insert(identifier, hits) 51 + let state = RateLimiterState(..state, hit_log: hit_log) 52 + 53 + // Check rate limits 54 + // TODO: optimize into a single loop 55 + let hits_last_hour = hits |> list.length() 56 + 57 + let hits_last_minute = 58 + hits 59 + |> list.filter(fn(hit) { hit >= timestamp - 60 }) 60 + |> list.length() 61 + 62 + let hits_last_second = 63 + hits 64 + |> list.filter(fn(hit) { hit >= timestamp - 1 }) 65 + |> list.length() 66 + 67 + let limit_reached = { 68 + case state.per_hour { 69 + Some(limit) -> hits_last_hour > limit 70 + None -> False 71 + } 72 + || case state.per_minute { 73 + Some(limit) -> hits_last_minute > limit 74 + None -> False 75 + } 76 + || case state.per_second { 77 + Some(limit) -> hits_last_second > limit 78 + None -> False 79 + } 80 + } 81 + 82 + case limit_reached { 83 + True -> process.send(client, Error(Nil)) 84 + False -> process.send(client, Ok(Nil)) 85 + } 86 + 87 + actor.continue(state) 88 + } 89 + } 90 + } 91 + 92 + /// Create a new rate limiter actor. 93 + /// 94 + pub fn new( 95 + per_second: Option(Int), 96 + per_minute: Option(Int), 97 + per_hour: Option(Int), 98 + ) -> Result(Subject(Message(id)), Nil) { 99 + let state = 100 + RateLimiterState( 101 + hit_log: dict.new(), 102 + per_second: per_second, 103 + per_minute: per_minute, 104 + per_hour: per_hour, 105 + ) 106 + actor.start(state, handle_message) 107 + |> result.nil_error 108 + } 109 + 110 + /// Log a hit for a given identifier. 111 + /// 112 + pub fn hit(subject: Subject(Message(id)), identifier: id) -> Result(Nil, Nil) { 113 + actor.call(subject, Hit(identifier, _), 10) 114 + } 115 + 116 + /// Stop the actor. 117 + /// 118 + pub fn stop(subject: Subject(Message(id))) { 119 + actor.send(subject, Shutdown) 120 + }