diff options
| author | ojacobson <ojacobson@noreply.codeberg.org> | 2025-10-28 18:41:41 +0100 |
|---|---|---|
| committer | ojacobson <ojacobson@noreply.codeberg.org> | 2025-10-28 18:41:41 +0100 |
| commit | 0ef69c7d256380e660edc45ace7f1d6151226340 (patch) | |
| tree | a6eea3af13f237393057aac1d80f024bc1b40b10 /src/login/app.rs | |
| parent | 58e6496558a01052537c5272169aea3e79ccbc8e (diff) | |
| parent | dc7074bbf39aff895ba52abd5e7b7e9bb643cf27 (diff) | |
Use freestanding structs for `App` components.
A "component" is a struct that provides domain-specific operations on the service. `App` largely acts as a way to obtain components for domain-specific operations: for example, given `let app: App = todo!();`, then `app.tokens()` provides a component (`Tokens`) that supports operations on authentication tokens, and `app.conversations()` provides a component (`Conversations`) that supports operations on conversations.
This has been a major piece of the server's internal organization for a long time. Historically, these components have been built as short-lived view structs, which hold onto their functional dependencies by reference. A given component was therefore bound to its source `App`, and had a lifetime bounded by the life of that `App` instance or reference.
This change turns components into freestanding structs - that is, they can outlive the `App` that provided them. They hold their dependencies by value, not by reference; `App` provides clones when creating a component, instead of borrowing its own state. For the functional dependencies we have today, cloning is a supported and cheap way to share access; details are documented in the individual commits.
I'm making this change because we're working on web push, and we discovered while prototyping that it will be useful to be able to support multiple distinct types of web push client. A running Pilcrow server would use a "real" client (which sends real HTTP requests to deliver push messages), while tests would use a client stub (which doesn't). However, to make that work, we'll need to make `App` generic over the client, and the resulting type parameter would then end up in every handler and in most other things that touch the `App` type. This refactoring dramatically reduces the number of places we mention the `App` type, by making most uses rely on specific components instead of relying on `App` generally.
There are still a few places that work on `App` generally, rather than on specific components, because an operation requires the use of two or more components.
I don't love all this cloning, even if I know in my head that it's fine. The alternatives that we looked at include:
* Provider traits, as we have for `Transaction`, that allow endpoints to specify that they want any type that can provide a `Tokens` or a `Conversation` instead of specifically an `App` (`App<PushClient>`). This is wordy enough that we've opted to punt on that approach for now.
* Accept the type parameter as the cost of doing business. This is still an open alternative.
* Use `dyn` dispatch instead of a type parameter for the push client. This is still an open alternative, though not one I love as we'd be incurring function call indirection without getting any generality out of it.
Merges freestanding-app-components into main.
Diffstat (limited to 'src/login/app.rs')
| -rw-r--r-- | src/login/app.rs | 10 |
1 files changed, 5 insertions, 5 deletions
diff --git a/src/login/app.rs b/src/login/app.rs index e471000..a2f9636 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -9,13 +9,13 @@ use crate::{ token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _}, }; -pub struct Logins<'a> { - db: &'a SqlitePool, - token_events: &'a Broadcaster, +pub struct Logins { + db: SqlitePool, + token_events: Broadcaster, } -impl<'a> Logins<'a> { - pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self { +impl Logins { + pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self { Self { db, token_events } } |
