1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Multi-channel notifications.
//!
//! One event, told to one person, over whichever channels apply: an email, an
//! in-app row, a live push. A [`Notification`] renders itself per channel, a
//! [`Notifier`] delivers it, and a [`Recipient`] says who to.
//!
//! Three channels ship today: mail; behind the `notifications-db` feature, an
//! in-app inbox backed by the application's own database; and behind
//! `notifications-broadcast`, a live push to whoever is connected right now.
//! The trait is shaped so later channels arrive without touching a line of
//! application code.
//!
//! The inbox and the live push are complements rather than alternatives. The
//! push is what a recipient sees without reloading; the inbox is what they see
//! when they arrive. A recipient who was offline missed the push and lost
//! nothing, provided the inbox was written too -- which is why an application
//! enabling `notifications-broadcast` alone should know it is choosing
//! best-effort delivery.
//!
//! # The live push is per process, and targeted by construction
//!
//! [`crate::realtime`] offers a single flat fanout, which is the wrong shape
//! for something addressed to a person: everything subscribed to a channel
//! receives everything published to it. So the broadcast channel is not a
//! channel but a [`BroadcastChannels`] resolver, recipient key to channel.
//! Targeting is then which channel the bytes go into rather than a filter
//! applied afterwards, and one recipient's payload has no path into another's
//! connection. [`PerRecipientChannels`] is the built-in resolver.
//!
//! The fanout underneath is a `tokio::sync::broadcast`, so it reaches the
//! connections held by *this* process and no others. An application running
//! more than one instance -- or sending notifications from a background
//! worker, which is a different process from the one holding the socket --
//! should treat the push as an optimisation over the inbox rather than a
//! delivery guarantee. The same limit is disclosed for the rest of
//! [`crate::realtime`] in `README.md` and `docs/src/deployment.md`; it is
//! repeated here because a notification is exactly the case where it bites.
//!
//! # Mail can be deferred; the other two cannot
//!
//! Behind `notifications-queue`, [`Notifier::queue`] writes the email to
//! [`crate::jobs`] instead of waiting for the SMTP server, and the request
//! stops paying for a TLS handshake to a machine it does not control. The
//! inbox row and the live push still run inline in the same call.
//!
//! That asymmetry is not an omission. Deferring the inbox would mean the
//! recipient who opens the application right after the event finds nothing
//! there, which is the exact failure writing the inbox first was meant to
//! prevent. And the push reaches the connections held by *this* process; a
//! worker holds none of them, so a queued push is a dropped one.
//!
//! The queue is at-least-once, so a worker that dies after handing a message
//! to the SMTP server but before marking the job complete leaves a job that
//! runs again -- and the email arrives twice. That is the cost of the
//! deferral, and it is disclosed rather than designed away, because writing
//! to a remote server and recording that you did cannot be made one
//! operation.
//!
//! # The inbox cannot be read across recipients
//!
//! [`DatabaseNotifications`] takes the recipient key on *every* method,
//! including the ones that already have an id: marking a notification read and
//! deleting one both carry `notifiable_key` in their `WHERE` clause. That is
//! not belt and braces. There is no statement in the store a handler can reach
//! with an id alone, so reading or dismissing someone else's notification is
//! not a rule that a handler has to remember to apply -- it is a query that
//! does not exist.
//!
//! # What is different from Laravel
//!
//! Laravel's notifications name their channels in `via()` and render them in
//! `toMail`/`toDatabase`/`toBroadcast`. Two places, and nothing keeps them
//! agreeing: a channel in `via()` with no method behind it throws at runtime,
//! and a method `via()` forgot is simply never called.
//!
//! Here there is no `via`. A notification reaches a channel exactly when the
//! method for that channel returns `Some`, so the list *is* the methods. The
//! per-recipient decision `via($notifiable)` exists to make is still there --
//! every method takes the [`Recipient`] -- but it is made in the one place
//! that also produces the content.
//!
//! # Nothing is delivered quietly
//!
//! Two outcomes that a notification system can easily hide are made visible
//! here. Asking for a channel the [`Notifier`] was never given returns
//! [`NotificationError::NotConfigured`] instead of skipping it, so a
//! forgotten `.with_mail(..)` at startup fails on the first send rather than
//! becoming password-reset emails that never arrive. And a successful send
//! returns a [`Delivery`] naming the channels that ran, so "reached nobody"
//! is a thing the caller can ask about rather than a silence identical to
//! success.
//!
//! # Example
//!
//! ```
//! use arcature::mail::{Mail, Mailer};
//! use arcature::notifications::{
//! Channel, MailContent, Notifiable, Notification, Notifier, Recipient,
//! };
//!
//! struct User {
//! id: i64,
//! email: String,
//! }
//!
//! impl Notifiable for User {
//! fn recipient(&self) -> Recipient {
//! Recipient::new(format!("user:{}", self.id)).email(&self.email)
//! }
//! }
//!
//! struct InvoicePaid {
//! amount_cents: i64,
//! }
//!
//! impl Notification for InvoicePaid {
//! fn to_mail(&self, recipient: &Recipient) -> Option<MailContent> {
//! recipient.email_address()?;
//! Some(
//! MailContent::new(
//! "Your invoice is paid",
//! format!("We received {}.{:02}.", self.amount_cents / 100, self.amount_cents % 100),
//! )
//! .html("<p>We received your payment.</p>"),
//! )
//! }
//! }
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let notifier = Notifier::new()
//! .with_mail(Mail::new(Mailer::capture_ok(), "billing@acme.test".parse()?));
//!
//! let ada = User { id: 42, email: "ada@example.com".into() };
//! let delivery = notifier.send(&ada, &InvoicePaid { amount_cents: 1250 }).await?;
//!
//! assert!(delivery.reached(Channel::Mail));
//! # Ok(())
//! # }
//! ```
// Needs `test-kit` for the "is a test database configured, and is it safe to
// write to" decision, which lives there and must have exactly one spelling.
pub use ;
pub use ;
pub use NotificationPool;
pub use ;
pub use ;
pub use ;
pub use ;
pub use DatabaseNotifications;
pub use ;