mail based rss feed aggregator
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

move all backend interaction into `backend` module the frontend now only gets a `backend.Reference` to use for anything backend related

ollie a30d61c8 48fa5bfc

+539 -873
+2 -2
dev/insert_test_data.gleam
··· 42 42 uri.parse("https://strawmelonjuice.com/feed.xml") 43 43 |> result.map(rss.new_location) 44 44 45 - let assert Ok(_) = database.add_feed(example_feed, into: database) 45 + let assert Ok(_) = database.add_feed(database, example_feed) 46 46 as "Failed to insert example feed" 47 47 48 48 io.println("Inserted example feed") 49 49 50 50 let assert Ok(_) = 51 - database.add_subscription(test_user, example_feed, into: database) 51 + database.add_subscription(database, test_user, example_feed) 52 52 as "Failed to insert example subscription" 53 53 io.println("Inserted example subscription") 54 54
+6 -10
src/eater.gleam
··· 19 19 import eater/backend 20 20 import eater/configuration 21 21 import eater/database 22 - import eater/fetcher 23 - import eater/sender 24 22 import eater/smtp 25 23 import eater/user 26 24 import eater/webserver ··· 28 26 import gleam/erlang/process 29 27 import gleam/otp/static_supervisor as supervisor 30 28 import gleam/result 31 - import group_registry 32 29 import sqlight 33 30 import woof 34 31 ··· 58 55 let fetcher_factory = process.new_name("fetcher_factory") 59 56 let fetcher_manager = process.new_name("fetcher_starter") 60 57 58 + let backend_reference = backend.new_reference(backend, database) 59 + 61 60 let assert Ok(_supervisor) = 62 61 supervisor.new(supervisor.RestForOne) 63 62 |> supervisor.add(backend.supervised( ··· 73 72 smtp_environment:, 74 73 )) 75 74 |> supervisor.add(webserver.supervised( 76 - database:, 77 - registry:, 78 - sender_factory:, 79 - fetcher_factory:, 75 + backend: backend_reference, 80 76 smtp_environment:, 81 77 configuration:, 82 78 )) ··· 91 87 database: sqlight.Connection, 92 88 smtp_environment: smtp.SmtpEnvironment, 93 89 configuration: configuration.AppConfig, 94 - ) -> Result(user.User, sqlight.Error) { 90 + ) -> Result(Nil, sqlight.Error) { 95 91 let sender_email = smtp.sender_email(smtp_environment) 96 92 97 93 use maybe_user <- result.try(database.user_by_email(database, sender_email)) 98 94 99 95 case maybe_user { 100 - Ok(user) -> Ok(user) 101 - Error(_) -> { 96 + [_, ..] -> Ok(Nil) 97 + [] -> { 102 98 let user = 103 99 user.new(sender_email, configuration.default_admin_password_hash) 104 100 |> user.promote
+201 -68
src/eater/backend.gleam
··· 29 29 import gleam/uri 30 30 import sqlight 31 31 import woof 32 + import youid/uuid 33 + 34 + /// log with module related structured data 35 + /// 36 + fn log( 37 + level: woof.Level, 38 + message: String, 39 + fields: List(#(String, String)), 40 + ) -> Nil { 41 + woof.new("BACKEND") 42 + |> woof.log(level, message, fields) 43 + } 32 44 33 45 // public stuff ----------------------------------------------------------------- 34 46 47 + /// get an instance of `Reference` to pass around 48 + /// 49 + pub fn new_reference(name, database) { 50 + Reference(name:, database:) 51 + } 52 + 53 + /// reference for the backend 54 + /// 55 + /// use with: 56 + /// - `new_subscription` 57 + /// - `remove_subscription` 58 + /// - `status` 59 + /// - `find_feed` 60 + /// - `restart_feed` 61 + /// - `refetch_feed` 62 + /// - `restart_user` 63 + /// - `fetch_users` 64 + /// - `subscriptions_for_user` 65 + /// - `...` 66 + /// 67 + pub opaque type Reference { 68 + Reference(name: Name, database: sqlight.Connection) 69 + } 70 + 71 + pub type Name = 72 + process.Name(Message) 73 + 35 74 pub type Names { 36 75 Names( 37 76 backend: process.Name(Message), ··· 79 118 // main api --------------------------------------------------------------------- 80 119 81 120 /// add a new subscription for a user 121 + /// - save to database 122 + /// - notify backend processes 82 123 /// 83 124 pub fn new_subscription( 84 - backend backend: process.Name(Message), 125 + backend backend: Reference, 85 126 user user: user.User, 86 127 feed feed: rss.Location, 87 - ) -> Nil { 88 - let backend = process.named_subject(backend) 128 + ) -> Result(Nil, Nil) { 129 + use _ <- result.try( 130 + database.add_feed(backend.database, feed) 131 + |> result.map_error(fn(error) { 132 + log(woof.Error, "Failed to add user to database", [ 133 + woof.field("user-email", user.email), 134 + woof.field("user-id", user.id |> uuid.to_string()), 135 + woof.field("details", string.inspect(error)), 136 + ]) 137 + }), 138 + ) 139 + 140 + use _ <- result.try( 141 + database.add_subscription(backend.database, user, feed) 142 + |> result.map_error(fn(error) { 143 + log(woof.Error, "Failed to add user to database", [ 144 + woof.field("user-email", user.email), 145 + woof.field("user-id", user.id |> uuid.to_string()), 146 + woof.field("details", string.inspect(error)), 147 + ]) 148 + }), 149 + ) 150 + 151 + let backend = process.named_subject(backend.name) 89 152 process.send(backend, NewSubscription(user, feed)) 153 + |> Ok 90 154 } 91 155 92 156 /// remove a subscription from a user 157 + /// 158 + /// - delete from database 159 + /// - notify backend processes 93 160 /// 94 161 pub fn remove_subscription( 95 - backend backend: process.Name(Message), 162 + backend backend: Reference, 96 163 user user: user.User, 97 164 feed feed: rss.Location, 98 - ) -> Nil { 99 - let backend = process.named_subject(backend) 165 + ) -> Result(Nil, Nil) { 166 + use _ <- result.try( 167 + database.delete_subscription(backend.database, user, feed) 168 + |> result.map_error(fn(error) { 169 + log(woof.Error, "Failed to add user to database", [ 170 + woof.field("user-email", user.email), 171 + woof.field("user-id", user.id |> uuid.to_string()), 172 + woof.field("details", string.inspect(error)), 173 + ]) 174 + }), 175 + ) 176 + 177 + let backend = process.named_subject(backend.name) 100 178 process.send(backend, RemoveSubscription(user, feed)) 179 + |> Ok 101 180 } 102 181 103 182 /// to be fleshed out as it becomes clearer what this entails ··· 107 186 /// 108 187 /// waits for the response from the backend 109 188 /// 110 - pub fn status(backend backend: process.Name(Message)) -> Result(Status, Nil) { 111 - let backend = process.named_subject(backend) 189 + pub fn status(backend backend: Reference) -> Result(Status, Nil) { 190 + let backend = process.named_subject(backend.name) 112 191 let send_to = process.new_subject() 113 192 114 193 process.send(backend, Status(send_to:)) ··· 118 197 119 198 /// restart the fetcher for this feed 120 199 /// 121 - pub fn restart_feed( 122 - backend backend: process.Name(Message), 123 - feed feed: rss.Location, 124 - ) -> Nil { 125 - let backend = process.named_subject(backend) 200 + pub fn restart_feed(backend backend: Reference, feed feed: rss.Location) -> Nil { 201 + let backend = process.named_subject(backend.name) 126 202 process.send(backend, RestartFeed(feed)) 127 203 } 128 204 129 205 /// trigger an early refetch for this feed 130 206 /// 131 - pub fn refetch( 132 - backend backend: process.Name(Message), 133 - feed feed: rss.Location, 134 - ) -> Nil { 135 - let backend = process.named_subject(backend) 207 + pub fn refetch_feed(backend backend: Reference, feed feed: rss.Location) -> Nil { 208 + let backend = process.named_subject(backend.name) 136 209 process.send(backend, Refetch(feed)) 137 210 } 138 211 139 212 /// restart the sender for this user 140 213 /// 141 - pub fn restart_user( 142 - backend backend: process.Name(Message), 143 - user user: user.User, 144 - ) -> Nil { 145 - let backend = process.named_subject(backend) 214 + pub fn restart_user(backend backend: Reference, user user: user.User) -> Nil { 215 + let backend = process.named_subject(backend.name) 146 216 process.send(backend, RestartUser(user)) 147 217 } 148 218 149 219 /// find a feed using a uri 150 220 /// 151 221 pub fn find_feed( 152 - backend backend: process.Name(Message), 222 + backend backend: Reference, 153 223 uri uri: uri.Uri, 154 224 ) -> Result(rss.Location, Nil) { 155 - let backend = process.named_subject(backend) 225 + case database.feed_by_link(uri, backend.database) { 226 + Error(_) -> Error(Nil) 227 + Ok([]) -> Ok(rss.new_location(uri)) 228 + Ok([feed, ..]) -> Ok(feed) 229 + } 230 + } 231 + 232 + /// find a user using an email 233 + /// 234 + pub fn find_user( 235 + backend backend: Reference, 236 + email email: String, 237 + ) -> Result(user.User, Nil) { 238 + case database.user_by_email(backend.database, email) { 239 + Error(_) -> Error(Nil) 240 + Ok([]) -> Error(Nil) 241 + Ok([user, ..]) -> Ok(user) 242 + } 243 + } 156 244 157 - let send_to = process.new_subject() 245 + /// add a new user 246 + /// 247 + /// Ok(Nil) -> added successfully 248 + /// Error(Nil) -> something went wrong 249 + /// 250 + pub fn new_user(backend: Reference, user: user.User) -> Result(Nil, Nil) { 251 + database.add_user(user, backend.database) 252 + |> result.map_error(fn(error) { 253 + log(woof.Error, "Failed to add user to database", [ 254 + woof.field("user-email", user.email), 255 + woof.field("user-id", user.id |> uuid.to_string()), 256 + woof.field("details", string.inspect(error)), 257 + ]) 258 + }) 259 + } 158 260 159 - process.send(backend, FindFeed(uri, send_to:)) 261 + /// get a list of all currently existing users 262 + /// 263 + pub fn fetch_users(backend: Reference) -> Result(List(user.User), Nil) { 264 + database.all_users(backend.database) 265 + |> result.map_error(fn(error) { 266 + log(woof.Error, "Failed to get users from database", [ 267 + woof.field("details", string.inspect(error)), 268 + ]) 269 + }) 270 + } 160 271 161 - process.receive(send_to, within: 1000) 162 - |> result.flatten() 272 + /// get all subscriptions for a user 273 + /// 274 + pub fn subscriptions_for_user( 275 + backend: Reference, 276 + user: user.User, 277 + ) -> Result(List(rss.Location), Nil) { 278 + database.feeds_for_user(backend.database, user) 279 + |> result.map_error(fn(error) { 280 + log(woof.Error, "Failed to get feeds for user", [ 281 + woof.field("user-email", user.email), 282 + woof.field("user-id", user.id |> uuid.to_string()), 283 + woof.field("details", string.inspect(error)), 284 + ]) 285 + }) 163 286 } 164 287 165 288 pub opaque type Message { ··· 206 329 /// restart the sender for this user 207 330 /// 208 331 RestartUser(user.User) 332 + } 209 333 210 - /// find a feed using a uri 211 - /// 212 - /// - check the database 213 - /// - found -> send the associated `rss.Location` to `send_to` 214 - /// - not found -> `rss.new_location` and send to `send_to` 215 - /// 216 - FindFeed(uri.Uri, send_to: process.Subject(Result(rss.Location, Nil))) 217 - // internal messages ---------------------------------------------------------- 334 + fn describe_message(message: Message) -> List(#(String, String)) { 335 + case message { 336 + NewSubscription(user, feed) -> [ 337 + woof.field("message", "NewSubscription"), 338 + woof.field("user-id", user.id |> uuid.to_string()), 339 + woof.field("user-email", user.email), 340 + woof.field("feed-id", feed.id |> uuid.to_string()), 341 + woof.field("feed-url", feed.link |> uri.to_string()), 342 + ] 343 + RemoveSubscription(user, feed) -> [ 344 + woof.field("message", "RemoveSubscription"), 345 + woof.field("user-id", user.id |> uuid.to_string()), 346 + woof.field("user-email", user.email), 347 + woof.field("feed-id", feed.id |> uuid.to_string()), 348 + woof.field("feed-url", feed.link |> uri.to_string()), 349 + ] 350 + Status(send_to: _) -> [ 351 + woof.field("message", "Status"), 352 + ] 353 + RestartFeed(feed) -> [ 354 + woof.field("message", "RestartFeed"), 355 + woof.field("feed-id", feed.id |> uuid.to_string()), 356 + woof.field("feed-url", feed.link |> uri.to_string()), 357 + ] 358 + Refetch(feed) -> [ 359 + woof.field("message", "Refetch"), 360 + woof.field("feed-id", feed.id |> uuid.to_string()), 361 + woof.field("feed-url", feed.link |> uri.to_string()), 362 + ] 363 + RestartUser(user) -> [ 364 + woof.field("message", "RestartUser"), 365 + woof.field("user-id", user.id |> uuid.to_string()), 366 + woof.field("user-email", user.email), 367 + ] 368 + } 218 369 } 219 370 220 371 type State { ··· 252 403 } 253 404 254 405 fn on_message(state: State, message: Message) -> actor.Next(State, Message) { 406 + case woof.is_enabled(woof.Debug) { 407 + True -> 408 + woof.log( 409 + state.logger, 410 + woof.Debug, 411 + "New message", 412 + describe_message(message), 413 + ) 414 + False -> Nil 415 + } 416 + 255 417 case message { 256 418 // external messages -------------------------------------------------------- 257 419 NewSubscription(user, feed) -> handle_new_subscription(state, user, feed) ··· 270 432 sender.restart_for(state.names.sender_manager, user) 271 433 actor.continue(state) 272 434 } 273 - FindFeed(uri, send_to:) -> { 274 - case database.feed_by_link(uri, state.database) { 275 - Error(_) -> Error(Nil) 276 - Ok([]) -> Ok(rss.new_location(uri)) 277 - Ok([feed, ..]) -> Ok(feed) 278 - } 279 - |> process.send(send_to, _) 280 - actor.continue(state) 281 - } 282 - // internal messages -------------------------------------------------------- 283 435 } 284 436 } 285 437 286 438 /// add a subscription for a user 287 439 /// 288 - /// - save to database 289 440 /// - notify sender 290 441 /// - start sender if not running 291 442 /// - start fetcher if not running ··· 295 446 user: user.User, 296 447 feed: rss.Location, 297 448 ) -> actor.Next(State, Message) { 298 - // - save to database 299 - use _ <- try_twice( 300 - fn() { database.add_feed(feed, state.database) }, 301 - otherwise: log_and_stop(state.logger, "Failed to add feed to database", [ 302 - woof.field("feed", feed.link |> uri.to_string()), 303 - ]), 304 - ) 305 - 306 449 // - notify sender 307 - pubsub.publish_subscribed_to_feed(user, feed, state.names.registry) 450 + sender.add_subscription(state.names.sender_manager, user, feed) 308 451 309 452 // - start sender if not running 310 453 sender.start_for(state.names.sender_manager, user) ··· 316 459 317 460 /// remove a subscription from a user 318 461 /// 319 - /// - delete from database 320 462 /// - notify sender 321 463 /// - if noone is subscribed 322 464 /// - stop fetcher ··· 327 469 user: user.User, 328 470 feed: rss.Location, 329 471 ) -> actor.Next(State, Message) { 330 - // - delete from database 331 - use _ <- try_twice( 332 - fn() { database.delete_subscription(user, feed, state.database) }, 333 - otherwise: log_and_stop(state.logger, "Failed delete subscription", [ 334 - woof.field("feed", feed.link |> uri.to_string()), 335 - woof.field("user", user.email), 336 - ]), 337 - ) 338 - 339 472 // - notify sender 340 - pubsub.publish_unsubscribed_from_feed(user, feed, state.names.registry) 473 + sender.remove_subscription(state.names.sender_manager, user, feed) 341 474 342 475 use subscribers <- try_twice( 343 476 fn() { database.subscription_count(feed, state.database) },
+20 -25
src/eater/database.gleam
··· 33 33 /// returns the feed for convenience 34 34 /// 35 35 pub fn add_feed( 36 - feed feed: rss.Location, 37 36 into on: sqlight.Connection, 37 + feed feed: rss.Location, 38 38 ) -> Result(rss.Location, sqlight.Error) { 39 39 let #(sql, with) = 40 40 sql.add_feed( ··· 142 142 pub fn add_user( 143 143 user user: user.User, 144 144 into on: sqlight.Connection, 145 - ) -> Result(user.User, sqlight.Error) { 145 + ) -> Result(Nil, sqlight.Error) { 146 146 let #(sql, with) = 147 147 sql.add_user( 148 148 id: user.id |> uuid.to_bit_array, ··· 157 157 let with = list.map(with, parrot_to_sqlight) 158 158 159 159 sqlight.query(sql, on:, with:, expecting: decode.success("")) 160 - |> result.replace(user) 160 + |> result.replace(Nil) 161 161 } 162 162 163 163 /// gets a list of all users from the database ··· 186 186 } 187 187 188 188 /// gets a specific user using the associated email 189 - /// the nested error contains the provided email 190 189 /// 191 190 pub fn user_by_email( 192 191 in on: sqlight.Connection, 193 192 email email: String, 194 - ) -> Result(Result(user.User, String), sqlight.Error) { 193 + ) -> Result(List(user.User), sqlight.Error) { 195 194 let #(sql, with, expecting) = sql.user_by_email(email:) 196 195 197 196 let with = list.map(with, parrot_to_sqlight) 198 197 199 198 use user <- result.try(sqlight.query(sql, on:, with:, expecting:)) 200 199 201 - case user { 202 - [] -> Error(email) 203 - [user, ..] -> { 204 - let assert Ok(id) = uuid.from_bit_array(user.id) 205 - as "invalid UUID from db UUID column?!" 200 + list.map(user, fn(user) { 201 + let assert Ok(id) = uuid.from_bit_array(user.id) 202 + as "invalid UUID from db UUID column?!" 206 203 207 - let is_admin = case user.is_admin { 208 - 1 -> True 209 - _ -> False 210 - } 204 + let is_admin = case user.is_admin { 205 + 1 -> True 206 + _ -> False 207 + } 211 208 212 - user.User(id, user.email, user.password_hash, is_admin:) 213 - |> Ok 214 - } 215 - } 209 + user.User(id, user.email, user.password_hash, is_admin:) 210 + }) 216 211 |> Ok 217 212 } 218 213 ··· 269 264 /// returns the feed on success for convenience 270 265 /// 271 266 pub fn add_subscription( 272 - user: user.User, 273 - feed: rss.Location, 274 - into on: sqlight.Connection, 267 + on: sqlight.Connection, 268 + user user: user.User, 269 + feed feed: rss.Location, 275 270 ) -> Result(rss.Location, sqlight.Error) { 276 271 let #(sql, with) = 277 272 sql.add_subscription( ··· 289 284 /// returns the feed in the `Ok()` for convenience 290 285 /// 291 286 pub fn delete_subscription( 292 - user: user.User, 293 - feed: rss.Location, 294 - database on: sqlight.Connection, 287 + on: sqlight.Connection, 288 + user user: user.User, 289 + feed feed: rss.Location, 295 290 ) -> Result(rss.Location, sqlight.Error) { 296 291 let #(sql, with) = 297 292 sql.delete_subsciption( ··· 308 303 /// feeds a given user is subscribed to 309 304 /// 310 305 pub fn feeds_for_user( 311 - for user: user.User, 312 306 in on: sqlight.Connection, 307 + for user: user.User, 313 308 ) -> Result(List(rss.Location), sqlight.Error) { 314 309 let #(sql, with, expecting) = sql.feeds_for_user(user.id |> uuid.to_bit_array) 315 310
+14 -18
src/eater/fetcher.gleam
··· 80 80 factory: FactoryName, 81 81 registry: pubsub.Registry, 82 82 database: sqlight.Connection, 83 - fetchers: dict.Dict(uuid.Uuid, #(process.Pid, process.Subject(Message))), 83 + fetchers: dict.Dict(uuid.Uuid, actor.Started(process.Subject(Message))), 84 84 ) 85 85 } 86 86 ··· 179 179 // if there is an existing sender for this 180 180 // tell it to shut down 181 181 case dict.get(state.fetchers, feed.id) { 182 - Ok(#(pid, _)) -> 183 - case process.is_alive(pid) { 184 - True -> process.send_exit(pid) 182 + Ok(actor) -> 183 + case process.is_alive(actor.pid) { 184 + True -> process.send_exit(actor.pid) 185 185 False -> Nil 186 186 } 187 187 Error(_) -> Nil ··· 190 190 let state = 191 191 ManagerState( 192 192 ..state, 193 - fetchers: dict.insert(state.fetchers, feed.id, #( 194 - started.pid, 195 - started.data, 196 - )), 193 + fetchers: dict.insert(state.fetchers, feed.id, started), 197 194 ) 198 195 199 196 log(woof.Info, "Fetcher has registered itself", [ ··· 225 222 } 226 223 227 224 case dict.get(state.fetchers, feed.id) { 228 - Ok(#(pid, _)) -> { 229 - case process.is_alive(pid) { 225 + Ok(actor) -> { 226 + case process.is_alive(actor.pid) { 230 227 True -> Nil 231 228 False -> start_new() 232 229 } ··· 238 235 } 239 236 StopFor(feed) -> { 240 237 case dict.get(state.fetchers, feed.id) { 241 - Ok(#(pid, _)) -> { 242 - case process.is_alive(pid) { 243 - True -> process.send_exit(pid) 238 + Ok(actor) -> { 239 + case process.is_alive(actor.pid) { 240 + True -> process.send_exit(actor.pid) 244 241 False -> Nil 245 242 } 246 243 } ··· 257 254 } 258 255 RecheckFor(feed) -> { 259 256 case dict.get(state.fetchers, feed.id) { 260 - Ok(#(pid, subject)) -> { 261 - case process.is_alive(pid) { 257 + Ok(actor) -> { 258 + case process.is_alive(actor.pid) { 262 259 True -> { 263 - process.send(subject, CheckFeeds) 260 + process.send(actor.data, CheckFeeds) 264 261 } 265 262 False -> Nil 266 263 } ··· 398 395 FailedToPersistFailure(sqlight.Error, FetchError) 399 396 } 400 397 398 + // TODO: clean up 401 399 /// tries to fetch a given feed and publish it to the group registry 402 400 /// 403 401 /// handles the failure cases and incrementsthe feed's cooldown accordingly 404 402 /// 405 - // TODO: clean up 406 - 407 403 fn handle_feed( 408 404 location: rss.Location, 409 405 state: State,
-39
src/eater/pubsub.gleam
··· 60 60 61 61 pub type Message { 62 62 FeedUpdate(update: rss.FeedUpdate) 63 - SubscriptionRemoved(rss.Location) 64 - SubscriptionAdded(rss.Location) 65 63 } 66 64 67 65 /// get the string 'channel' for a given `rss.Location` ··· 77 75 } 78 76 79 77 // publishing ------------------------------------------------------------------- 80 - 81 - /// publish a user unsubscribing from a feed 82 - /// 83 - pub fn publish_unsubscribed_from_feed( 84 - user user: user.User, 85 - from feed: rss.Location, 86 - in registry: Registry, 87 - ) -> Nil { 88 - let registry = group_registry.get_registry(registry) 89 - 90 - let members = group_registry.members(registry, user_channel(user)) 91 - 92 - list.map(members, process.send(_, SubscriptionRemoved(feed))) 93 - 94 - log(woof.Info, "Published unsubscribe", [ 95 - woof.field("user", user.email), 96 - woof.field("feed", feed.link |> uri.to_string()), 97 - ]) 98 - } 99 - 100 - /// publish a user subscribing to a feed 101 - /// 102 - pub fn publish_subscribed_to_feed( 103 - user user: user.User, 104 - from feed: rss.Location, 105 - in registry: Registry, 106 - ) -> Nil { 107 - let registry = group_registry.get_registry(registry) 108 - 109 - let members = group_registry.members(registry, user_channel(user)) 110 - 111 - list.map(members, process.send(_, SubscriptionAdded(feed))) 112 - log(woof.Info, "Published subscription", [ 113 - woof.field("user", user.email), 114 - woof.field("feed", feed.link |> uri.to_string()), 115 - ]) 116 - } 117 78 118 79 /// publish an `rss.FeedUpdate` for a given `rss.Location` in a given registry 119 80 ///
+74 -21
src/eater/sender.gleam
··· 53 53 /// reexport of the underlying `process.Name` for convenience 54 54 /// 55 55 pub type FactoryName = 56 - process.Name(factory.Message(Start, Nil)) 56 + process.Name(factory.Message(Start, process.Subject(Message))) 57 57 58 58 pub fn factory( 59 59 name factory: FactoryName, ··· 90 90 registry: pubsub.Registry, 91 91 database: sqlight.Connection, 92 92 smtp_environment: smtp.SmtpEnvironment, 93 - senders: dict.Dict(uuid.Uuid, process.Pid), 93 + senders: dict.Dict(uuid.Uuid, actor.Started(process.Subject(Message))), 94 94 ) 95 95 } 96 96 ··· 99 99 /// start all senders 100 100 StartAll 101 101 /// a sender has started and is registering itself 102 - SenderStarted(Result(actor.Started(Nil), actor.StartError), user.User) 102 + SenderStarted( 103 + Result(actor.Started(process.Subject(Message)), actor.StartError), 104 + user.User, 105 + ) 103 106 104 107 // external commands ---------------------------------------------------------- 105 108 /// start a sender for a given user ··· 108 111 StopFor(user.User) 109 112 /// restart the sender for a given user 110 113 RestartFor(user.User) 114 + /// add a subscription for this user 115 + AddSubscription(user.User, rss.Location) 116 + /// remove a subscription for this user 117 + RemoveSubscription(user.User, rss.Location) 111 118 } 112 119 113 120 /// start a sender for a given user ··· 131 138 |> process.send(RestartFor(user)) 132 139 } 133 140 141 + /// add a subscription for this user 142 + /// 143 + pub fn add_subscription( 144 + manager: ManagerName, 145 + user: user.User, 146 + feed: rss.Location, 147 + ) { 148 + process.named_subject(manager) 149 + |> process.send(AddSubscription(user, feed)) 150 + } 151 + 152 + /// remove a subscription for this user 153 + /// 154 + pub fn remove_subscription( 155 + manager: ManagerName, 156 + user: user.User, 157 + feed: rss.Location, 158 + ) { 159 + process.named_subject(manager) 160 + |> process.send(RemoveSubscription(user, feed)) 161 + } 162 + 134 163 /// starts senders for all existing users on startup 135 164 /// then also tracks every started sender and handles starting / stopping and restarting 136 165 /// ··· 183 212 // if there is an existing sender for this 184 213 // tell it to shut down 185 214 case dict.get(state.senders, user.id) { 186 - Ok(pid) -> 187 - case process.is_alive(pid) { 188 - True -> process.send_exit(pid) 215 + Ok(actor) -> 216 + case process.is_alive(actor.pid) { 217 + True -> process.send_exit(actor.pid) 189 218 False -> Nil 190 219 } 191 220 Error(_) -> Nil ··· 194 223 let state = 195 224 ManagerState( 196 225 ..state, 197 - senders: dict.insert(state.senders, user.id, started.pid), 226 + senders: dict.insert(state.senders, user.id, started), 198 227 ) 199 228 200 229 log(woof.Info, "Sender has registered itself", [ ··· 233 262 } 234 263 235 264 case dict.get(state.senders, user.id) { 236 - Ok(pid) -> { 237 - case process.is_alive(pid) { 265 + Ok(actor) -> { 266 + case process.is_alive(actor.pid) { 238 267 True -> Nil 239 268 False -> start_new() 240 269 } ··· 246 275 } 247 276 StopFor(user) -> { 248 277 case dict.get(state.senders, user.id) { 249 - Ok(pid) -> { 250 - case process.is_alive(pid) { 251 - True -> process.send_exit(pid) 278 + Ok(actor) -> { 279 + case process.is_alive(actor.pid) { 280 + True -> process.send_exit(actor.pid) 252 281 False -> Nil 253 282 } 254 283 } ··· 263 292 264 293 actor.continue(state) 265 294 } 295 + AddSubscription(user, feed) -> { 296 + case dict.get(state.senders, user.id) { 297 + Ok(actor) -> { 298 + process.send(actor.data, NewSubscription(feed)) 299 + } 300 + Error(_) -> Nil 301 + } 302 + 303 + actor.continue(state) 304 + } 305 + RemoveSubscription(user, feed) -> { 306 + case dict.get(state.senders, user.id) { 307 + Ok(actor) -> { 308 + process.send(actor.data, DropSubscription(feed)) 309 + } 310 + Error(_) -> Nil 311 + } 312 + 313 + actor.continue(state) 314 + } 266 315 } 267 316 }) 268 317 |> actor.named(name) ··· 309 358 310 359 // actor ------------------------------------------------------------------------ 311 360 312 - fn sender(args: Start) -> Result(actor.Started(Nil), actor.StartError) { 361 + fn sender( 362 + args: Start, 363 + ) -> Result(actor.Started(process.Subject(Message)), actor.StartError) { 313 364 let Start(database:, registry:, user:, smtp_environment:, manager:) = args 314 365 315 366 let actor_started = ··· 331 382 332 383 actor.initialised(state) 333 384 |> actor.selecting(state.selector) 385 + |> actor.returning(self) 334 386 |> Ok 335 387 }) 336 388 |> actor.on_message(on_message) ··· 345 397 346 398 // message ---------------------------------------------------------------------- 347 399 348 - type Message { 400 + pub opaque type Message { 349 401 /// sent once at the start to fetch all feeds 350 402 /// 351 403 GetFeeds 352 404 PubSubMessage(pubsub.Message) 353 405 Retry(message: pubsub.Message, attempt: Int) 406 + 407 + NewSubscription(rss.Location) 408 + DropSubscription(rss.Location) 354 409 } 355 410 356 411 // message handling ------------------------------------------------------------- ··· 417 472 } 418 473 } 419 474 } 420 - PubSubMessage(pubsub.SubscriptionRemoved(feed)) -> { 421 - let state = drop_feed(state, feed) 422 - actor.continue(state) 423 - } 424 - PubSubMessage(pubsub.SubscriptionAdded(feed)) -> { 475 + NewSubscription(feed) -> { 425 476 let state = add_feed(state, feed) 426 477 427 478 actor.continue(state) 428 479 |> actor.with_selector(state.selector) 480 + } 481 + DropSubscription(feed) -> { 482 + let state = drop_feed(state, feed) 483 + actor.continue(state) 429 484 } 430 485 Retry(message: pubsub.FeedUpdate(update:), attempt:) if attempt < 5 -> { 431 486 let handled = handle_feed_update(state, update) ··· 464 519 ]) 465 520 actor.continue(state) 466 521 } 467 - Retry(message:, ..) -> 468 - actor.stop_abnormal(string.inspect(message) <> "cannot be retried") 469 522 } 470 523 } 471 524
+213 -476
src/eater/ui/main_ui.gleam
··· 16 16 // This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. [cite: 5] 17 17 // See the Licence for the specific language governing permissions and limitations. [cite: 6] 18 18 19 + import eater/backend 19 20 import eater/configuration 20 - import eater/database 21 21 import eater/feed/rss 22 - import eater/fetcher 23 - import eater/pubsub 24 - import eater/sender 25 22 import eater/smtp 26 23 import eater/ui/toaster 27 24 import eater/user ··· 38 35 import gleam/int 39 36 import gleam/list 40 37 import gleam/option.{None, Some} 41 - import gleam/otp/actor 42 38 import gleam/result 43 39 import gleam/string 44 40 import gleam/uri ··· 50 46 import lustre/element/keyed 51 47 import lustre/event 52 48 import lustre/portal 53 - import sqlight 54 49 import woof 55 50 import youid/uuid 56 51 ··· 69 64 pub opaque type Context { 70 65 Context( 71 66 csrf_token: String, 72 - database: sqlight.Connection, 73 - registry: pubsub.Registry, 74 - sender_factory: sender.FactoryName, 75 - fetcher_factory: fetcher.FactoryName, 67 + backend: backend.Reference, 76 68 smtp_environment: smtp.SmtpEnvironment, 77 69 configuration: configuration.AppConfig, 78 70 toasts: List(toaster.Toast), ··· 94 86 /// 95 87 pub fn new_context( 96 88 csrf_token _csrf_token: String, 97 - database database: sqlight.Connection, 98 - registry registry: pubsub.Registry, 99 - sender_factory sender_factory: sender.FactoryName, 100 - fetcher_factory fetcher_factory: fetcher.FactoryName, 89 + backend backend: backend.Reference, 101 90 smtp_environment smtp_environment: smtp.SmtpEnvironment, 102 91 configuration configuration: configuration.AppConfig, 103 92 ) -> Context { ··· 107 96 108 97 Context( 109 98 csrf_token: session_id, 110 - database:, 111 - registry:, 112 - sender_factory:, 113 - fetcher_factory:, 99 + backend:, 114 100 smtp_environment:, 115 101 configuration:, 116 102 toasts: [], ··· 138 124 139 125 // logged in 140 126 User(context: Context, data: UserData) 141 - Admin(context: Context, data: AdminData) 127 + Admin(context: Context, data: UserData, admin_data: AdminData) 142 128 } 143 129 144 130 type UserData { ··· 150 136 } 151 137 152 138 type AdminData { 153 - AdminData( 154 - self: user.User, 155 - subscriptions: List(rss.Location), 156 - users: List(user.User), 157 - add_subscription_form: Form(uri.Uri), 158 - ) 139 + AdminData(users: List(user.User)) 159 140 } 160 141 161 142 /// describe a model using structured data ··· 189 170 woof.field("user-email", self.email), 190 171 woof.int_field("subscription-count", subscriptions |> list.length()), 191 172 ] 192 - Admin( 193 - context: _, 194 - data: AdminData(self:, subscriptions:, users: _, add_subscription_form: _), 195 - ) -> [ 173 + Admin(..) -> [ 196 174 woof.field("model", "Admin"), 197 - woof.field("user-email", self.email), 198 - woof.int_field("subscription-count", subscriptions |> list.length()), 175 + woof.field("user-email", model.data.self.email), 176 + woof.int_field( 177 + "subscription-count", 178 + model.data.subscriptions |> list.length(), 179 + ), 199 180 ] 200 181 } 201 182 } ··· 204 185 // login 205 186 UserSubmittedLoginForm(result: Result(FormUser, Form(FormUser))) 206 187 ServerVerifiedLogin(valid: Bool, user: user.User) 207 - DatabaseReturnedUser( 208 - in_db: Result(Result(user.User, String), sqlight.Error), 209 - from_form: FormUser, 210 - ) 188 + BackendReturnedUser(user: Result(user.User, Nil), from_form: FormUser) 211 189 212 190 // signup 213 191 UserClickedGotoSignUp ··· 222 200 223 201 // toast 224 202 TimerDismissedToast 225 - /// 226 - ///```gleam 227 - ///Result(Result(user.User, email), db_error) 228 - ///``` 229 - ServerCreatedNewUser(user: user.User) 230 - DatabasePersistedNewUser(Result(user.User, sqlight.Error)) 203 + BackendCreatedNewUser(user: Result(user.User, Nil)) 231 204 UserClickedGotoLogIn 232 - DatabaseReturnedAllUsers(Result(List(user.User), sqlight.Error)) 233 - DatabaseReturnedSubscriptions(Result(List(rss.Location), sqlight.Error)) 205 + BackendReturnedAllUsers(Result(List(user.User), Nil)) 206 + BackendReturnedSubscriptions(Result(List(rss.Location), Nil)) 234 207 UserClickedUnsubscribe(rss.Location) 235 - DatabaseDeletedSubscription(Result(rss.Location, sqlight.Error)) 236 - UnsubscribedInBackend 208 + UnsubscribedInBackend(Result(rss.Location, Nil)) 237 209 UserSubmittedSubscription(Result(uri.Uri, Form(uri.Uri))) 238 - DatabaseReturnedFeed(Result(List(rss.Location), sqlight.Error), uri.Uri) 239 - DatabaseAddedFeed(Result(rss.Location, sqlight.Error)) 240 - DatabasePersistedSubscription(Result(rss.Location, sqlight.Error)) 241 - ServerCreatedNewFeed(rss.Location) 242 - ServerStartedNewSender( 243 - Result(actor.Started(Nil), actor.StartError), 244 - user.User, 245 - ) 246 - ServerStartedNewFetcher( 247 - Result(actor.Started(Nil), actor.StartError), 248 - rss.Location, 249 - ) 250 - SubscribedInBackend(rss.Location) 210 + SubscribedInBackend(Result(rss.Location, Nil)) 211 + BackendReturnedFeed(Result(rss.Location, Nil)) 251 212 } 252 213 253 214 /// describe a message using structured data ··· 263 224 woof.field("message", "UserSubmittedEmail"), 264 225 woof.field("status", "error"), 265 226 ] 266 - DatabaseReturnedUser(Ok(Ok(_)), _) -> [ 267 - woof.field("message", "DatabaseReturnedUser"), 227 + BackendReturnedUser(Ok(user), _) -> [ 228 + woof.field("message", "BackendReturnedUser"), 268 229 woof.field("status", "ok"), 269 - woof.field("details", "found"), 230 + woof.field("user-email", user.email), 231 + woof.field("user-id", user.id |> uuid.to_string()), 270 232 ] 271 - DatabaseReturnedUser(Ok(Error(_)), _) -> [ 272 - woof.field("message", "DatabaseReturnedUser"), 273 - woof.field("status", "ok"), 274 - woof.field("status", "not-found"), 275 - ] 276 - DatabaseReturnedUser(Error(_), _) -> [ 277 - woof.field("message", "DatabaseReturnedUser"), 233 + BackendReturnedUser(Error(_), _) -> [ 234 + woof.field("message", "BackendReturnedUser"), 278 235 woof.field("status", "error"), 279 236 ] 280 - ServerCreatedNewUser(user) -> [ 281 - woof.field("message", "ServerCreatedNewUser"), 237 + BackendCreatedNewUser(Ok(user)) -> [ 238 + woof.field("message", "BackendCreatedNewUser"), 239 + woof.field("status", "ok"), 282 240 woof.field("email", user.email), 283 241 woof.field("id", user.id |> uuid.to_string()), 284 242 ] 285 - DatabasePersistedNewUser(Ok(_)) -> [ 286 - woof.field("message", "DatabaseAddedNewUser"), 287 - woof.field("status", "ok"), 288 - ] 289 - DatabasePersistedNewUser(Error(_)) -> [ 290 - woof.field("message", "DatabaseAddedNewUser"), 243 + BackendCreatedNewUser(Error(_)) -> [ 244 + woof.field("message", "BackendCreatedNewUser"), 291 245 woof.field("status", "error"), 292 246 ] 293 247 UserSubmittedSignUpForm(Ok(user)) -> [ ··· 336 290 TimerDismissedToast -> [ 337 291 woof.field("message", "TimerDismissedToast"), 338 292 ] 339 - DatabaseReturnedAllUsers(Ok(_)) -> [ 340 - woof.field("message", "DatabaseReturnedAllUsers"), 293 + BackendReturnedAllUsers(Ok(_)) -> [ 294 + woof.field("message", "BackendReturnedAllUsers"), 341 295 woof.field("status", "ok"), 342 296 ] 343 - DatabaseReturnedAllUsers(Error(db_error)) -> [ 344 - woof.field("message", "DatabaseReturnedAllUsers"), 297 + BackendReturnedAllUsers(Error(db_error)) -> [ 298 + woof.field("message", "BackendReturnedAllUsers"), 345 299 woof.field("status", "error"), 346 300 woof.field("details", string.inspect(db_error)), 347 301 ] 348 - DatabaseReturnedSubscriptions(Ok(_)) -> [ 349 - woof.field("message", "DatabaseReturnedSubscriptions"), 302 + BackendReturnedSubscriptions(Ok(subs)) -> [ 303 + woof.field("message", "BackendReturnedSubscriptions"), 350 304 woof.field("status", "ok"), 305 + woof.int_field("subscriptions-returned", list.length(subs)), 351 306 ] 352 - DatabaseReturnedSubscriptions(Error(db_error)) -> [ 307 + BackendReturnedSubscriptions(Error(db_error)) -> [ 353 308 woof.field("message", "DatabaseReturnedSubscriptions"), 354 309 woof.field("status", "error"), 355 310 woof.field("details", string.inspect(db_error)), ··· 358 313 woof.field("message", "UserClickedUnsubscribe"), 359 314 woof.field("feed", location.link |> uri.to_string()), 360 315 ] 361 - DatabaseDeletedSubscription(Ok(location)) -> [ 362 - woof.field("message", "DatabaseDeletedSubscription"), 316 + UnsubscribedInBackend(Ok(feed)) -> [ 317 + woof.field("message", "UnsubscribedInBackend"), 363 318 woof.field("status", "ok"), 364 - woof.field("feed", location.link |> uri.to_string()), 365 - ] 366 - DatabaseDeletedSubscription(Error(db_error)) -> [ 367 - woof.field("message", "DatabaseDeletedSubscription"), 368 - woof.field("status", "error"), 369 - woof.field("details", string.inspect(db_error)), 319 + woof.field("feed-url", feed.link |> uri.to_string()), 320 + woof.field("feed-id", feed.id |> uuid.to_string()), 370 321 ] 371 - UnsubscribedInBackend -> [ 322 + UnsubscribedInBackend(Error(_)) -> [ 372 323 woof.field("message", "UnsubscribedInBackend"), 324 + woof.field("status", "error"), 373 325 ] 374 - SubscribedInBackend(feed) -> [ 326 + SubscribedInBackend(Ok(feed)) -> [ 375 327 woof.field("message", "SubscribedInBackend"), 376 328 woof.field("status", "ok"), 377 329 woof.field("feed", feed.link |> uri.to_string()), 330 + woof.field("feed-id", feed.id |> uuid.to_string()), 331 + ] 332 + SubscribedInBackend(Error(_)) -> [ 333 + woof.field("message", "SubscribedInBackend"), 334 + woof.field("status", "error"), 378 335 ] 379 336 UserSubmittedSubscription(Ok(link)) -> [ 380 337 woof.field("message", "UserSubmittedSubscription"), ··· 385 342 woof.field("message", "UserSubmittedSubscription"), 386 343 woof.field("status", "error"), 387 344 ] 388 - DatabaseReturnedFeed(Ok([]), _feed) -> [ 389 - woof.field("message", "DatabaseReturnedFeed"), 345 + BackendReturnedFeed(Ok(feed)) -> [ 346 + woof.field("message", "BackendReturnedFeed"), 390 347 woof.field("status", "ok"), 391 - ] 392 - DatabaseReturnedFeed(Ok([feed, ..]), _feed) -> [ 393 - woof.field("message", "DatabaseReturnedFeed"), 394 - woof.field("status", "ok"), 395 - woof.field("feed-link", feed.link |> uri.to_string()), 348 + woof.field("feed-url", feed.link |> uri.to_string()), 396 349 woof.field("feed-id", feed.id |> uuid.to_string()), 397 350 ] 398 - DatabaseReturnedFeed(Error(db_error), feed) -> [ 399 - woof.field("message", "DatabaseReturnedFeed"), 351 + BackendReturnedFeed(Error(_)) -> [ 352 + woof.field("message", "BackendReturnedFeed"), 400 353 woof.field("status", "error"), 401 - woof.field("feed", feed |> uri.to_string()), 402 - woof.field("details", string.inspect(db_error)), 403 354 ] 404 - DatabaseAddedFeed(Ok(feed)) -> [ 405 - woof.field("message", "DatabaseAddedFeed"), 406 - woof.field("status", "ok"), 407 - woof.field("feed-link", feed.link |> uri.to_string()), 408 - woof.field("feed-id", feed.id |> uuid.to_string()), 409 - ] 410 - DatabaseAddedFeed(Error(db_error)) -> [ 411 - woof.field("message", "DatabaseAddedFeed"), 412 - woof.field("status", "error"), 413 - woof.field("details", string.inspect(db_error)), 414 - ] 415 - DatabasePersistedSubscription(Ok(feed)) -> [ 416 - woof.field("message", "DatabasePersistedSubscription"), 417 - woof.field("status", "ok"), 418 - woof.field("feed-link", feed.link |> uri.to_string()), 419 - woof.field("feed-id", feed.id |> uuid.to_string()), 420 - ] 421 - DatabasePersistedSubscription(Error(db_error)) -> [ 422 - woof.field("message", "DatabasePersistedSubscription"), 423 - woof.field("status", "error"), 424 - woof.field("details", string.inspect(db_error)), 425 - ] 426 - ServerCreatedNewFeed(feed) -> [ 427 - woof.field("message", "ServerCreatedNewFeed"), 428 - woof.field("feed-link", feed.link |> uri.to_string()), 429 - woof.field("feed-id", feed.id |> uuid.to_string()), 430 - ] 431 - ServerStartedNewSender(_, _) -> todo 432 - ServerStartedNewFetcher(_, _) -> todo 433 355 } 434 356 } 435 357 ··· 502 424 UserSubmittedOneTimePassword(Ok(password)), 503 425 ConfirmOneTimePassword(password_to_confirm:, context: _, user:, ..) 504 426 if password == password_to_confirm 505 - -> #(model, create_new_user(user)) 427 + -> #(model, create_new_user(model.context, user)) 506 428 507 - ServerCreatedNewUser(user), _ -> #( 508 - model, 509 - persist_new_user(user, model.context.database), 510 - ) 511 - DatabasePersistedNewUser(Ok(user)), _ -> #( 429 + BackendCreatedNewUser(Ok(user)), _ -> #( 512 430 User(model.context, UserData(user, [], add_subscription_form())), 513 - fetch_logged_in_data(model.context.database, user), 431 + fetch_logged_in_data(model.context, user), 514 432 ) 515 - DatabasePersistedNewUser(Error(_)), _ -> 516 - log_and_toast( 517 - model, 518 - message, 519 - "Database error", 520 - Some("Something went wrong"), 521 - "I ran into an issue :/", 522 - ) 433 + BackendCreatedNewUser(Error(_)), _ -> { 434 + let toast = 435 + toaster.Toast( 436 + title: Some("Something went wrong"), 437 + message: "I ran into an issue creating your account :/\nTry again in a bit", 438 + options: toast.default_options(toast.Warning), 439 + ) 440 + 441 + do_toast(toast, model) 442 + } 523 443 524 444 // doesnt match, let the user retry up to 3 times 525 445 UserSubmittedOneTimePassword(Ok(_)), ··· 585 505 ) 586 506 UserSubmittedLoginForm(Ok(login)), _ -> #( 587 507 model, 588 - fetch_user_from_database(model.context, login), 508 + fetch_user(model.context, login), 589 509 ) 590 - DatabaseReturnedUser(Ok(Ok(user)), from_form), _ -> #( 510 + BackendReturnedUser(Ok(user), from_form), _ -> #( 591 511 model, 592 512 verify_login(user, from_form), 593 513 ) 594 - DatabaseReturnedUser(Ok(Error(_)), _), _ -> { 514 + BackendReturnedUser(Error(_), _), _ -> { 595 515 case model.context.configuration.allow_signups { 596 516 True -> { 597 517 let toast = ··· 615 535 } 616 536 } 617 537 } 618 - DatabaseReturnedUser(Error(_), _), _ -> 619 - log_and_toast( 620 - model, 621 - message, 622 - "Database error", 623 - None, 624 - "I ran into a problem", 625 - ) 626 538 627 539 // login admin 628 540 ServerVerifiedLogin(valid: True, user:), _ if user.is_admin -> #( 629 - Admin(model.context, AdminData(user, [], [], add_subscription_form())), 630 - fetch_logged_in_data(model.context.database, user), 541 + Admin( 542 + model.context, 543 + UserData(user, [], add_subscription_form()), 544 + AdminData([]), 545 + ), 546 + fetch_logged_in_data(model.context, user), 631 547 ) 632 548 // login normal user 633 549 ServerVerifiedLogin(valid: True, user:), _ -> #( 634 550 User(model.context, UserData(user, [], add_subscription_form())), 635 - fetch_logged_in_data(model.context.database, user), 551 + fetch_logged_in_data(model.context, user), 636 552 ) 637 553 638 554 ServerVerifiedLogin(valid: False, user: _), _ -> { ··· 660 576 661 577 // main ui bits ------------------------------------------------------------- 662 578 // user data arrived 663 - DatabaseReturnedSubscriptions(Ok(subscriptions)), User(context:, data:) -> #( 664 - User(context:, data: UserData(..data, subscriptions:)), 579 + BackendReturnedSubscriptions(Ok(subscriptions)), _ -> #( 580 + update_user_data(model, fn(data) { UserData(..data, subscriptions:) }), 665 581 effect.none(), 666 582 ) 667 - DatabaseReturnedSubscriptions(Ok(subscriptions)), Admin(context:, data:) -> #( 668 - Admin(context:, data: AdminData(..data, subscriptions:)), 669 - effect.none(), 670 - ) 671 - // nu uh 672 - DatabaseReturnedSubscriptions(Ok(_)), _ -> #(model, effect.none()) 673 583 674 584 // user data didnt arrive 675 585 // nu uh 676 - DatabaseReturnedSubscriptions(Error(_)), _ -> 586 + BackendReturnedSubscriptions(Error(_)), _ -> 677 587 log_and_toast( 678 588 model, 679 589 message, ··· 682 592 "I failed to get your subscriptions. Try again in a bit.", 683 593 ) 684 594 685 - DatabaseReturnedAllUsers(Ok(users)), Admin(context:, data:) -> #( 686 - Admin(context:, data: AdminData(..data, users:)), 595 + BackendReturnedAllUsers(Ok(users)), _ -> #( 596 + update_admin_data(model, fn(_) { AdminData(users:) }), 687 597 effect.none(), 688 598 ) 689 - DatabaseReturnedAllUsers(Error(_)), Admin(..) -> 599 + BackendReturnedAllUsers(Error(_)), _ -> 690 600 log_and_toast( 691 601 model, 692 602 message, ··· 695 605 "I failed to fetch all users. Try again in a bit.", 696 606 ) 697 607 698 - // nu uh 699 - DatabaseReturnedAllUsers(_), _ -> #(model, effect.none()) 700 - 701 608 // removing subscriptions --------------------------------------------------- 702 - UserClickedUnsubscribe(feed), Admin(data: AdminData(self:, ..), ..) 703 - | UserClickedUnsubscribe(feed), User(data: UserData(self:, ..), ..) 704 - -> #(model, remove_subscription_from_database(model.context, self, feed)) 705 - // nu uh 706 - UserClickedUnsubscribe(_), _ -> #(model, effect.none()) 707 - 708 - DatabaseDeletedSubscription(Ok(feed)), User(context:, data:) -> #( 709 - User( 710 - context:, 711 - data: UserData( 712 - ..data, 713 - subscriptions: drop_subscription_from_list(data.subscriptions, feed), 714 - ), 715 - ), 716 - notify_backend_of_subscription_removal( 717 - model.context, 718 - model.data.self, 719 - feed, 609 + UserClickedUnsubscribe(feed), _ -> #( 610 + model, 611 + with_user_data( 612 + model, 613 + do: fn(data) { 614 + remove_subscription_in_backend(model.context, data.self, feed) 615 + }, 616 + otherwise: effect.none(), 720 617 ), 721 618 ) 722 619 723 - DatabaseDeletedSubscription(Ok(feed)), Admin(context:, data:) -> #( 724 - Admin( 725 - context:, 726 - data: AdminData( 620 + UnsubscribedInBackend(Ok(feed)), _ -> #( 621 + update_user_data(model, fn(data) { 622 + UserData( 727 623 ..data, 728 624 subscriptions: drop_subscription_from_list(data.subscriptions, feed), 729 - ), 730 - ), 731 - notify_backend_of_subscription_removal( 732 - model.context, 733 - model.data.self, 734 - feed, 735 - ), 625 + ) 626 + }), 627 + effect.none(), 736 628 ) 737 - // nu uh 738 - DatabaseDeletedSubscription(Ok(_)), _ -> #(model, effect.none()) 739 - DatabaseDeletedSubscription(Error(_)), _ -> 740 - log_and_toast( 741 - model, 742 - message, 743 - "Database error", 744 - Some("Failed to unsubscribe"), 745 - "I failed to unsubscribe you from that feed. Try again in a bit.", 746 - ) 747 - 748 - UnsubscribedInBackend, _ -> #(model, effect.none()) 629 + UnsubscribedInBackend(Error(_)), _ -> { 630 + let toast = 631 + toaster.Toast( 632 + title: None, 633 + message: "I couldnt unsubscribe you from that feed. Try again in a bit", 634 + options: toast.default_options(toast.Warning), 635 + ) 636 + do_toast(toast, model) 637 + } 749 638 750 639 // adding subscriptions ----------------------------------------------------- 751 640 // new subscriptions; invalid url 752 - UserSubmittedSubscription(Error(form)), User(context:, data:) -> #( 753 - User(context:, data: UserData(..data, add_subscription_form: form)), 754 - effect.none(), 755 - ) 756 - UserSubmittedSubscription(Error(form)), Admin(context:, data:) -> #( 757 - Admin(context:, data: AdminData(..data, add_subscription_form: form)), 641 + UserSubmittedSubscription(Error(form)), _ -> #( 642 + update_user_data(model, fn(data) { 643 + UserData(..data, add_subscription_form: form) 644 + }), 758 645 effect.none(), 759 646 ) 760 - // nu uh 761 - UserSubmittedSubscription(Error(_)), _ -> #(model, effect.none()) 762 647 763 648 // new subscriptions; valid url 764 649 UserSubmittedSubscription(Ok(url)), _ -> #( 765 650 model, 766 - get_feed_from_database(model.context, url), 651 + get_feed_from_backend(model.context, url), 767 652 ) 768 - 769 - // feed already exists in database, just need to add a subscription 770 - DatabaseReturnedFeed(Ok([feed, ..]), _), User(data: UserData(self:, ..), ..) 771 - | DatabaseReturnedFeed(Ok([feed, ..]), _), 772 - Admin(data: AdminData(self:, ..), ..) 773 - -> #(model, persist_subscription(model.context, self, feed)) 774 - 775 - // feed doesnt exist yet, create a new `rss.Location` for it 776 - DatabaseReturnedFeed(Ok([]), uri), _ -> #(model, create_new_feed(uri)) 777 - 778 - // nu uh 779 - DatabaseReturnedFeed(Ok(_), _), _ -> #(model, effect.none()) 780 - 781 - DatabaseReturnedFeed(Error(_), _), _ -> 782 - log_and_toast( 653 + BackendReturnedFeed(Ok(feed)), _ -> #( 654 + model, 655 + with_user_data( 783 656 model, 784 - message, 785 - "Database error", 786 - Some("Failed to subscribe"), 787 - "I failed to subscribe you to that feed. Try again in a bit.", 788 - ) 789 - 790 - // we made a new `rss.Location`, lets save it 791 - ServerCreatedNewFeed(feed), _ -> #( 792 - model, 793 - persist_new_feed(model.context, feed), 657 + do: fn(data) { 658 + add_subscription_in_backend(model.context, data.self, feed) 659 + }, 660 + otherwise: effect.none(), 661 + ), 794 662 ) 795 663 796 - // the new `rss.Location` has been saved to the database 797 - // lets also save a new subscription with it 798 - DatabaseAddedFeed(Ok(feed)), User(context:, data: UserData(self:, ..)) -> #( 799 - model, 800 - persist_subscription(context, self, feed), 801 - ) 802 - DatabaseAddedFeed(Ok(feed)), Admin(context:, data: AdminData(self:, ..)) -> #( 803 - model, 804 - persist_subscription(context, self, feed), 664 + BackendReturnedFeed(Error(_)), _ -> { 665 + let toast = 666 + toaster.Toast( 667 + title: None, 668 + message: "I couldnt add that feed. Try again maybe?", 669 + options: toast.default_options(toast.Info), 670 + ) 671 + do_toast(toast, model) 672 + } 673 + 674 + SubscribedInBackend(Ok(feed)), _ -> #( 675 + update_user_data(model, fn(data) { 676 + UserData(..data, subscriptions: [feed, ..data.subscriptions]) 677 + }), 678 + effect.none(), 805 679 ) 806 - // nu uh 807 - DatabaseAddedFeed(Ok(_)), _ -> #(model, effect.none()) 808 680 809 - DatabaseAddedFeed(Error(_)), _ -> 810 - log_and_toast( 811 - model:, 812 - message:, 813 - error_message: "Database error", 814 - toast_title: Some("Welp"), 815 - toast_message: "I ran into a problem while subscribing you to that feed. Try again in a bit.", 816 - ) 681 + SubscribedInBackend(Error(_)), _ -> { 682 + let toast = 683 + toaster.Toast( 684 + title: None, 685 + message: "Something went wrong while subscribing to that feed. Try again.", 686 + options: toast.default_options(toast.Warning), 687 + ) 688 + do_toast(toast, model) 689 + } 690 + } 691 + } 817 692 818 - // add the feed to the list, tell the backend about it and clear the form 819 - DatabasePersistedSubscription(Ok(feed)), 820 - User(context:, data: UserData(self:, subscriptions:, ..)) 821 - -> #( 822 - User( 823 - context:, 824 - data: UserData( 825 - ..model.data, 826 - subscriptions: [feed, ..subscriptions], 827 - add_subscription_form: add_subscription_form(), 828 - ), 829 - ), 830 - subscribe_in_backend(context, self, feed), 831 - ) 832 - DatabasePersistedSubscription(Ok(feed)), 833 - Admin(context:, data: AdminData(self:, subscriptions:, ..)) 834 - -> #( 835 - Admin( 836 - context:, 837 - data: AdminData( 838 - ..model.data, 839 - subscriptions: [feed, ..subscriptions], 840 - add_subscription_form: add_subscription_form(), 841 - ), 842 - ), 843 - subscribe_in_backend(context, self, feed), 844 - ) 845 - // nu uh 846 - DatabasePersistedSubscription(Ok(_)), _ -> #(model, effect.none()) 693 + fn update_admin_data( 694 + model: Model, 695 + update_data: fn(AdminData) -> AdminData, 696 + ) -> Model { 697 + case model { 698 + Admin(context:, data:, admin_data:) -> 699 + Admin(context:, data:, admin_data: update_data(admin_data)) 700 + _ -> model 701 + } 702 + } 847 703 848 - DatabasePersistedSubscription(Error(_)), _ -> 849 - log_and_toast( 850 - model:, 851 - message:, 852 - error_message: "Database error", 853 - toast_title: Some("Ooppsie"), 854 - toast_message: "I failed to subscribe you to that feed right now. Try again in a bit.", 855 - ) 856 - SubscribedInBackend(_), _ -> #(model, effect.none()) 704 + fn update_user_data(model: Model, update_data: fn(UserData) -> UserData) { 705 + case model { 706 + User(context:, data:) -> User(context:, data: update_data(data)) 707 + Admin(context:, data:, admin_data:) -> 708 + Admin(context:, data: update_data(data), admin_data:) 709 + _ -> model 710 + } 711 + } 857 712 858 - ServerStartedNewSender(_, _), _ -> todo 859 - ServerStartedNewFetcher(_, _), _ -> todo 713 + fn with_user_data( 714 + model: Model, 715 + do do: fn(UserData) -> a, 716 + otherwise otherwise: a, 717 + ) { 718 + case model { 719 + User(data:, ..) | Admin(data:, ..) -> do(data) 720 + _ -> otherwise 860 721 } 861 722 } 862 723 ··· 881 742 do_toast(toast, model) 882 743 } 883 744 884 - /// persist a new subscription to the database 885 - /// 886 - /// `DatabasePersistedSubscription` 887 - /// 888 - fn persist_subscription( 889 - context: Context, 890 - user: user.User, 891 - feed: rss.Location, 892 - ) -> Effect(Message) { 893 - use dispatch <- effect.from 894 - 895 - database.add_subscription(user, feed, context.database) 896 - |> DatabasePersistedSubscription 897 - |> dispatch 898 - } 899 - 900 - /// create a new `rss.Location` using a uri 901 - /// 902 - /// `ServerCreatedNewFeed` 903 - /// 904 - fn create_new_feed(feed: uri.Uri) -> Effect(Message) { 905 - use dispatch <- effect.from 906 - 907 - rss.new_location(feed) 908 - |> ServerCreatedNewFeed 909 - |> dispatch 910 - } 911 - 912 - /// persist a new feed to the database 913 - /// 914 - /// `DatabaseAddedFeed` 915 - /// 916 - fn persist_new_feed(context: Context, feed: rss.Location) -> Effect(Message) { 917 - use dispatch <- effect.from 918 - 919 - database.add_feed(feed, context.database) 920 - |> DatabaseAddedFeed 921 - |> dispatch 922 - } 923 - 924 - /// get a feed from the database using a uri 745 + /// get a feed using a uri 925 746 /// 926 - /// `DatabaseReturnedFeed` 747 + /// `BackendReturnedFeed` 927 748 /// 928 - fn get_feed_from_database(context: Context, uri: uri.Uri) -> Effect(Message) { 749 + fn get_feed_from_backend(context: Context, uri: uri.Uri) -> Effect(Message) { 929 750 use dispatch <- effect.from 930 751 931 - database.feed_by_link(uri, context.database) 932 - |> DatabaseReturnedFeed(uri) 752 + backend.find_feed(context.backend, uri:) 753 + |> BackendReturnedFeed 933 754 |> dispatch 934 755 } 935 756 936 - /// notify the currently running backend components 937 - /// and start the ones that arnt running 938 - /// 939 - fn subscribe_in_backend( 940 - context: Context, 941 - user: user.User, 942 - feed: rss.Location, 943 - ) -> Effect(Message) { 944 - effect.batch([ 945 - ensure_sender_running(context, user), 946 - ensure_fetcher_running(context, feed), 947 - notify_backend_of_subscription(context, user, feed), 948 - ]) 949 - } 950 - 951 757 /// notify the backend of a new subscription 952 758 /// 953 759 /// `SubscribedInBackend` 954 760 /// 955 - fn notify_backend_of_subscription( 761 + fn add_subscription_in_backend( 956 762 context: Context, 957 763 user: user.User, 958 764 feed: rss.Location, 959 765 ) -> Effect(Message) { 960 766 use dispatch <- effect.from 961 767 962 - pubsub.publish_subscribed_to_feed(user, feed, context.registry) 963 - 964 - dispatch(SubscribedInBackend(feed)) 965 - } 966 - 967 - /// ensure the sender for this user is running 968 - /// 969 - fn ensure_sender_running(context: Context, user: user.User) -> Effect(Message) { 970 - let Context(database:, registry:, sender_factory:, smtp_environment:, ..) = 971 - context 972 - 973 - case pubsub.subscriber_count_user(user, registry) { 974 - [] -> { 975 - // use dispatch <- effect.from 976 - // sender.start_new( 977 - // sender_factory, 978 - // sender.Start(database:, registry:, user:, smtp_environment:), 979 - // ) 980 - // |> ServerStartedNewSender(user) 981 - // |> dispatch 982 - todo 983 - } 984 - [_, ..] -> effect.none() 985 - } 986 - } 987 - 988 - /// ensure the fetcher for this feed is running 989 - /// 990 - fn ensure_fetcher_running( 991 - context: Context, 992 - feed: rss.Location, 993 - ) -> Effect(Message) { 994 - let Context(database:, registry:, fetcher_factory:, ..) = context 995 - 996 - case pubsub.subscriber_count_feed(feed, registry) { 997 - [] -> { 998 - use dispatch <- effect.from 999 - 1000 - todo as "this is nolonger needed with the new backend" 1001 - // fetcher.start_new( 1002 - // fetcher_factory, 1003 - // fetcher.Start(feed:, registry:, database:), 1004 - // ) 1005 - // |> ServerStartedNewFetcher(feed) 1006 - // |> dispatch 1007 - } 1008 - [_, ..] -> effect.none() 1009 - } 768 + backend.new_subscription(context.backend, user, feed) 769 + |> result.replace(feed) 770 + |> SubscribedInBackend 771 + |> dispatch 1010 772 } 1011 773 1012 774 /// handles unsubscribing in the backend 1013 775 /// 1014 776 /// `UnsubscribedInBackend` 1015 777 /// 1016 - fn notify_backend_of_subscription_removal( 778 + fn remove_subscription_in_backend( 1017 779 context: Context, 1018 780 user: user.User, 1019 781 feed: rss.Location, 1020 782 ) -> Effect(Message) { 1021 783 use dispatch <- effect.from 1022 784 1023 - pubsub.publish_unsubscribed_from_feed(user, feed, context.registry) 1024 - 1025 - dispatch(UnsubscribedInBackend) 785 + backend.remove_subscription(context.backend, user:, feed:) 786 + |> result.replace(feed) 787 + |> UnsubscribedInBackend 788 + |> dispatch 1026 789 } 1027 790 1028 791 /// remove a specific subscription from a list ··· 1036 799 }) 1037 800 } 1038 801 1039 - /// remove a given user -> feed relation from the database 1040 - /// 1041 - /// `DatabaseDeletedSubscription` 1042 - /// 1043 - fn remove_subscription_from_database( 1044 - context: Context, 1045 - user: user.User, 1046 - feed: rss.Location, 1047 - ) -> Effect(Message) { 1048 - use dispatch <- effect.from 1049 - 1050 - database.delete_subscription(user, feed, context.database) 1051 - |> DatabaseDeletedSubscription 1052 - |> dispatch 1053 - } 1054 - 1055 802 /// fetch data required when logged in 1056 803 /// user stuff 1057 804 /// admin stuff if applicaple 1058 805 /// 1059 - fn fetch_logged_in_data( 1060 - database: sqlight.Connection, 1061 - user: user.User, 1062 - ) -> Effect(Message) { 806 + fn fetch_logged_in_data(context: Context, user: user.User) -> Effect(Message) { 1063 807 let admin_data = case user.is_admin { 1064 - True -> fetch_user_list(database) 808 + True -> fetch_user_list(context) 1065 809 False -> effect.none() 1066 810 } 1067 811 1068 812 effect.batch([ 1069 - fetch_user_subscriptions(database, user), 813 + fetch_user_subscriptions(context, user), 1070 814 admin_data, 1071 815 ]) 1072 816 } ··· 1076 820 /// `DatabaseReturnedSubscriptions` 1077 821 /// 1078 822 fn fetch_user_subscriptions( 1079 - database: sqlight.Connection, 823 + context: Context, 1080 824 user: user.User, 1081 825 ) -> Effect(Message) { 1082 826 use dispatch <- effect.from 1083 827 1084 - database.feeds_for_user(user, database) 1085 - |> DatabaseReturnedSubscriptions 828 + backend.subscriptions_for_user(context.backend, user) 829 + |> BackendReturnedSubscriptions 1086 830 |> dispatch 1087 831 } 1088 832 ··· 1090 834 /// 1091 835 /// `DatabaseReturnedAllUsers` 1092 836 /// 1093 - fn fetch_user_list(database: sqlight.Connection) -> Effect(Message) { 837 + fn fetch_user_list(context: Context) -> Effect(Message) { 1094 838 use dispatch <- effect.from 1095 839 1096 - database.all_users(database) 1097 - |> DatabaseReturnedAllUsers 840 + backend.fetch_users(context.backend) 841 + |> BackendReturnedAllUsers 1098 842 |> dispatch 1099 843 } 1100 844 1101 845 /// create a new user from a `FormUser` 1102 846 /// 1103 - /// `ServerCreatedNewUser` 847 + /// `BackendCreatedNewUser` 1104 848 /// 1105 - fn create_new_user(user: FormUser) -> Effect(Message) { 849 + fn create_new_user(context: Context, user: FormUser) -> Effect(Message) { 1106 850 use dispatch <- effect.from 1107 851 1108 - user.new(user.email, user.hash_password(user.password)) 1109 - |> ServerCreatedNewUser 1110 - |> dispatch 1111 - } 852 + let user = user.new(user.email, user.hash_password(user.password)) 1112 853 1113 - /// persist a user to the database 1114 - /// 1115 - /// `DatabasePersistedNewUser` 1116 - /// 1117 - fn persist_new_user( 1118 - user: user.User, 1119 - database: sqlight.Connection, 1120 - ) -> Effect(Message) { 1121 - use dispatch <- effect.from 1122 - 1123 - database.add_user(user, database) 1124 - |> DatabasePersistedNewUser 854 + backend.new_user(context.backend, user) 855 + |> result.replace(user) 856 + |> BackendCreatedNewUser 1125 857 |> dispatch 1126 858 } 1127 859 ··· 1144 876 1145 877 /// fetch a user from the database using an email 1146 878 /// 1147 - /// `DatabaseReturnedUser` 879 + /// `BackendReturnedUser` 1148 880 /// 1149 - fn fetch_user_from_database(data: Context, user: FormUser) -> Effect(Message) { 881 + fn fetch_user(context: Context, user: FormUser) -> Effect(Message) { 1150 882 use dispatch <- effect.from 1151 883 1152 - database.user_by_email(data.database, user.email) 1153 - |> DatabaseReturnedUser(user) 884 + backend.find_user(context.backend, user.email) 885 + |> BackendReturnedUser(user) 1154 886 |> dispatch 1155 887 } 1156 888 ··· 1219 951 ) 1220 952 1221 953 User(context:, data:) -> view_logged_in_user(context, data) 1222 - Admin(context:, data:) -> view_logged_in_admin(context, data) 954 + Admin(context:, data:, admin_data:) -> 955 + view_logged_in_admin(context, data, admin_data) 1223 956 }, 1224 957 // button.button([event.on_click(SpawnToast)], [element.text("spawn toast")]), 1225 958 portal.to("body", [], [ ··· 1299 1032 1300 1033 /// logged in admin view 1301 1034 /// 1302 - fn view_logged_in_admin(_context: Context, data: AdminData) -> Element(Message) { 1035 + fn view_logged_in_admin( 1036 + _context: Context, 1037 + data: UserData, 1038 + _admin_data: AdminData, 1039 + ) -> Element(Message) { 1303 1040 html.div([attribute.class("container")], [ 1304 1041 html.h2([], [element.text("Hello " <> data.self.email <> " (admin)")]), 1305 1042 html.hr([]),
+9 -24
src/eater/webserver.gleam
··· 1 1 //// webserver 2 2 //// the webserver used to serve the lustre ui `main_ui` 3 3 4 + import eater/backend 4 5 import eater/configuration 5 - import eater/fetcher 6 - import eater/pubsub 7 - import eater/sender 8 6 import eater/smtp 9 7 import eater/ui/main_ui 10 8 import ewe ··· 15 13 import gleam/http/response.{type Response} 16 14 import gleam/json 17 15 import gleam/list 18 - import gleam/option.{None, Some} 16 + import gleam/option.{None} 19 17 import gleam/result 20 18 import lustre 21 19 import lustre/attribute.{attribute} 22 20 import lustre/element 23 21 import lustre/element/html.{html} 24 22 import lustre/server_component 25 - import sqlight 26 23 import youid/uuid 27 24 28 25 pub fn supervised( 29 - database database, 30 - registry registry: pubsub.Registry, 31 - sender_factory sender_factory: sender.FactoryName, 32 - fetcher_factory fetcher_factory: fetcher.FactoryName, 33 - smtp_environment smtp_environment, 26 + backend backend: backend.Reference, 27 + smtp_environment smtp_environment: smtp.SmtpEnvironment, 34 28 configuration configuration: configuration.AppConfig, 35 29 ) { 36 30 // TODO: proper CSRF protection with unique token per connection ··· 45 39 ["ws"] -> 46 40 serve_component( 47 41 request:, 48 - registry:, 49 - sender_factory:, 50 - fetcher_factory:, 51 - database:, 42 + backend:, 52 43 smtp_environment:, 53 44 configuration:, 54 45 expected_csrf_token: csrf_token, ··· 200 191 201 192 fn serve_component( 202 193 request request: Request(ewe.Connection), 203 - registry registry: pubsub.Registry, 204 - sender_factory sender_factory: sender.FactoryName, 205 - fetcher_factory fetcher_factory: fetcher.FactoryName, 206 - database database: sqlight.Connection, 194 + backend backend: backend.Reference, 207 195 smtp_environment smtp_environment: smtp.SmtpEnvironment, 208 196 configuration configuration: configuration.AppConfig, 209 197 expected_csrf_token expected_csrf_token: String, ··· 233 221 login, 234 222 with: main_ui.new_context( 235 223 csrf_token: token, 236 - database:, 237 - registry:, 238 - sender_factory:, 239 - fetcher_factory:, 224 + backend:, 240 225 smtp_environment:, 241 226 configuration: configuration, 242 227 ), ··· 254 239 ewe.upgrade_websocket( 255 240 request, 256 241 on_init:, 257 - handler: handle_login_websocket_message, 242 + handler: handle_lustre_websocket_message, 258 243 on_close: fn(_connection, state) { 259 244 // When the websocket connection closes, we need to also shut down the server 260 245 // component runtime. If we forget to do this we'll end up with a memory leak ··· 274 259 } 275 260 } 276 261 277 - fn handle_login_websocket_message( 262 + fn handle_lustre_websocket_message( 278 263 connection: ewe.WebsocketConnection, 279 264 state: LoginSocket, 280 265 message: ewe.WebsocketMessage(server_component.ClientMessage(main_ui.Message)),
-190
test/eater_test.gleam
··· 1 - import eater/feed/rss 2 - import eater/pubsub 3 - import eater/sender 4 - import eater/smtp 5 - import eater/user 6 - import gleam/erlang/process 7 - import gleam/int 8 - import gleam/option 9 - import gleam/otp/actor 10 - import gleam/otp/factory_supervisor 11 - import gleam/otp/static_supervisor 12 - import gleam/string 13 - import gleam/uri 14 1 import gleeunit 15 - import group_registry 16 - import simplifile 17 - import sqlight 18 - import youid/uuid 19 2 20 3 pub fn main() -> Nil { 21 4 gleeunit.main() 22 5 } 23 - 24 - type PubsubWrapper { 25 - PubsubWrapper(pubsub.Message) 26 - } 27 - 28 - fn selector_test() { 29 - echo "selector_test start" 30 - 31 - let subject_a: process.Subject(String) = process.new_subject() 32 - let subject_b: process.Subject(Int) = process.new_subject() 33 - 34 - let selector = 35 - process.new_selector() 36 - |> process.select(subject_a) 37 - 38 - process.send(subject_a, "something") 39 - 40 - let selector = 41 - selector 42 - |> process.select_map(subject_b, int.to_string) 43 - 44 - process.send(subject_b, 123) 45 - 46 - let assert Ok("something") = process.selector_receive(selector, 100) 47 - let assert Ok("123") = process.selector_receive(selector, 100) 48 - 49 - echo "selector_test end" 50 - } 51 - 52 - fn subscription_test() { 53 - echo "subscription_test start" 54 - let self = process.self() 55 - let feed = rss.new_location(uri.empty) 56 - 57 - let registry = process.new_name("registry") 58 - 59 - let assert Ok(_) = pubsub.start(registry) 60 - 61 - let selector = process.new_selector() 62 - 63 - let selector = 64 - pubsub.select_feed( 65 - feed:, 66 - in: registry, 67 - self:, 68 - selector:, 69 - handler: PubsubWrapper, 70 - ) 71 - 72 - pubsub.publish_feed_update(feed_update("1"), feed, registry) 73 - 74 - let selector = 75 - pubsub.select_feed( 76 - feed:, 77 - in: registry, 78 - self:, 79 - selector:, 80 - handler: PubsubWrapper, 81 - ) 82 - 83 - pubsub.publish_feed_update(feed_update(""), feed, registry) 84 - 85 - let assert Ok(PubsubWrapper(pubsub.FeedUpdate(update:))) = 86 - process.selector_receive(selector, 1000) 87 - 88 - assert update == feed_update("1") 89 - 90 - let assert Ok(PubsubWrapper(pubsub.FeedUpdate(update:))) = 91 - process.selector_receive(selector, 1000) 92 - 93 - assert update == feed_update("") 94 - echo "subscription_test end" 95 - } 96 - 97 - pub fn sender_subscription_test() { 98 - echo "sender_subscription_test start" 99 - 100 - let _ = simplifile.delete_file("db/data/test-db.sqlite3") 101 - let assert Ok(_) = 102 - simplifile.copy_file( 103 - "db/data/test-db.sqlite3.bak", 104 - "db/data/test-db.sqlite3", 105 - ) 106 - 107 - use database <- sqlight.with_connection("db/data/test-db.sqlite3") 108 - 109 - let feed = rss.new_location(uri.empty) 110 - let user = user.new("test", <<>>) 111 - 112 - let registry = process.new_name("registry") 113 - let factory = process.new_name("factory") 114 - let starter = process.new_name("starter") 115 - 116 - let assert Ok(_) = 117 - static_supervisor.new(static_supervisor.RestForOne) 118 - |> static_supervisor.add(pubsub.supervised(registry)) 119 - |> static_supervisor.add(sender.factory( 120 - factory, 121 - starter:, 122 - registry:, 123 - database:, 124 - smtp_environment: test_smtp_env(), 125 - )) 126 - |> static_supervisor.start() 127 - as "failed to start supervisor" 128 - 129 - // let assert Ok(_) = 130 - // sender.start_new( 131 - // factory, 132 - // sender.Start( 133 - // database:, 134 - // registry:, 135 - // user:, 136 - // smtp_environment: test_smtp_env(), 137 - // ), 138 - // ) 139 - 140 - pubsub.publish_feed_update(feed_update("0"), feed, registry) 141 - 142 - process.sleep(200) 143 - 144 - pubsub.publish_subscribed_to_feed(user, feed, registry) 145 - 146 - process.sleep(200) 147 - 148 - pubsub.publish_feed_update(feed_update("1"), feed, registry) 149 - 150 - process.sleep(200) 151 - 152 - pubsub.publish_unsubscribed_from_feed(user, feed, registry) 153 - 154 - process.sleep(200) 155 - 156 - pubsub.publish_feed_update(feed_update("2"), feed, registry) 157 - 158 - process.sleep(200) 159 - 160 - pubsub.publish_subscribed_to_feed(user, feed, registry) 161 - 162 - process.sleep(200) 163 - 164 - pubsub.publish_feed_update(feed_update("3"), feed, registry) 165 - 166 - process.sleep(200) 167 - 168 - echo "sender_subscription_test end" 169 - } 170 - 171 - fn test_smtp_env() -> smtp.SmtpEnvironment { 172 - smtp.SmtpEnvironment( 173 - host: "", 174 - port: 0, 175 - username: "", 176 - password: "", 177 - sender_email: option.None, 178 - sender_name: option.None, 179 - ) 180 - } 181 - 182 - fn feed_update(diff) { 183 - rss.FeedUpdate( 184 - rss.Item(title: "test", link: "test" <> diff, description: "test"), 185 - channel: rss.ChannelDetails( 186 - title: "test", 187 - link: "test", 188 - description: "test", 189 - ), 190 - ) 191 - } 192 - 193 - fn feed_channel(feed: rss.Location) -> String { 194 - feed.id |> uuid.to_string() 195 - }