pub struct Notifications { /* private fields */ }Expand description
Push subscriptions, preferences, fan-out and delivery.
Implementations§
Source§impl Notifications
impl Notifications
Sourcepub fn new() -> Self
pub fn new() -> Self
No categories (a venture must declare its own), five attempts before a transient failure is given up on, fifty rows per drain, eight of them in flight at a time, and three device take-overs per account per hour.
Sourcepub fn categories(
self,
names: impl IntoIterator<Item = impl Into<String>>,
) -> Self
pub fn categories( self, names: impl IntoIterator<Item = impl Into<String>>, ) -> Self
Declares several categories, all on by default and none carrying a badge — the common case.
Sourcepub fn max_attempts(self, attempts: u32) -> Self
pub fn max_attempts(self, attempts: u32) -> Self
How many delivery attempts a transient failure gets before the row
is dead-lettered (default 5; NOTIFICATIONS_MAX_ATTEMPTS).
Sourcepub fn drain_batch(self, rows: u64) -> Self
pub fn drain_batch(self, rows: u64) -> Self
How many outbox rows one drain pass leases (default 50;
NOTIFICATIONS_DRAIN_BATCH).
Sourcepub fn drain_concurrency(self, rows: u32) -> Self
pub fn drain_concurrency(self, rows: u32) -> Self
How many of those rows are in flight at a time (default 8;
NOTIFICATIONS_DRAIN_CONCURRENCY).
Each in-flight row is one outbound provider request, so this is the
knob for a runtime that counts subrequests. 1 restores a strictly
sequential drain.
Sourcepub fn rehome_max_per_hour(self, devices: u32) -> Self
pub fn rehome_max_per_hour(self, devices: u32) -> Self
How many devices one account may take over from other accounts in
an hour (default 3; NOTIFICATIONS_REHOME_MAX_PER_HOUR).
A device that signs into another account re-homes, which is the
behaviour a shared tablet needs — and the reason a token must not
be treated as proof of anything. Whoever holds a leaked
FCM/APNs token can present it with their own valid bearer and take
the device over: the previous owner stops receiving, and the
caller’s notifications start arriving on someone else’s phone.
Nothing in a push token distinguishes the two cases, so the module
bounds them instead: over this many take-overs in an hour the
registration answers REHOME_LIMIT (429), every one of them
emits EVENT_SUBSCRIPTION_REHOMED, and a notification queued for
the previous owner is dropped rather than delivered to the new one.
0 refuses every cross-account re-home, for a venture whose
devices are never shared. Sign-out (DELETE /subscriptions/{id})
still frees the device for the next account, because it deletes the
row rather than moving it.
Sourcepub fn inbox_retention_days(self, days: u32) -> Self
pub fn inbox_retention_days(self, days: u32) -> Self
How long a read or archived inbox row is kept before the scheduled
tick deletes it (default 90 days; 0 keeps them forever).
Only rows the account has finished with. An unread row is still waiting to be seen however old it is, and deleting it would be the module deciding the account missed its chance.
Sourcepub fn email_max_per_window(self, mails: u32) -> Self
pub fn email_max_per_window(self, mails: u32) -> Self
At most this many mails per account per category per window
(default 5 an hour; 0 stops the channel).
A cap rather than a queue: over it the mail is dropped, not deferred. Deferring would deliver the backlog the moment the window rolled, which is the flood the cap exists to prevent.
Sourcepub fn email_window_secs(self, seconds: u32) -> Self
pub fn email_window_secs(self, seconds: u32) -> Self
How long that window is, in seconds (default one hour).
Sourcepub fn catalog(self, catalog: impl Catalog + 'static) -> Self
pub fn catalog(self, catalog: impl Catalog + 'static) -> Self
The venture’s own strings, for Message::Localizable callers
(issue #190).
use cratefield_i18n::FluentCatalog;
let catalog = FluentCatalog::builder()
.default_locale("en")
.locale("en", include_str!("../locales/en.ftl"))
.locale("id", include_str!("../locales/id.ftl"))
.build()?;
Notifications::new().catalog(catalog).messages(["booking-confirmed"])Built once, by the venture, and shared: the bundles are parsed
at cold start, not per request. A venture that never calls this
pays nothing at all — no catalog is built, no locale is read, and a
rendered Notification travels exactly as it did before this
existed.
A catalog the venture already holds behind an Arc — because it
shares one with its own mail templates, say.
Sourcepub fn default_locale(self, tag: &str) -> Self
pub fn default_locale(self, tag: &str) -> Self
The locale a recipient who has expressed none is written to
(NOTIFICATIONS_DEFAULT_LOCALE).
Defaults to the catalog’s own default locale, which is usually the right answer; set it only when they differ.
§Panics
If tag is not a BCP 47 language tag. A composition is code a
developer wrote and compiled, so a typo here should stop the
venture rather than quietly write everybody in English.
Sourcepub fn messages(self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self
pub fn messages(self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self
The message ids this venture promises are translated into every locale its catalog declares.
A catalog cannot know which messages a caller will name, so the
venture says. Every id listed here must have a .title and a
.body in every configured locale, or the module refuses to
start and fz doctor lists what is missing — which is the whole
point: a translation gap found at deploy time is a pull request,
and the same gap found at delivery time is a person reading
booking-confirmed.title on a lock screen.
Sourcepub fn missing_translations(&self) -> Vec<String>
pub fn missing_translations(&self) -> Vec<String>
Every locale: key.attribute the catalog cannot render, for the
messages this venture declared with Notifications::messages.
Empty for a venture with no catalog and for one whose catalog is complete. Identifiers only — never a rendered string, which is built from a caller’s arguments about a person.
Sourcepub fn mailer_probe(
self,
probe: impl Fn(&dyn Config) -> bool + Send + Sync + 'static,
) -> Self
pub fn mailer_probe( self, probe: impl Fn(&dyn Config) -> bool + Send + Sync + 'static, ) -> Self
How the venture answers “is a mailer wired?”, for the same
production check as Notifications::transport_probe.
Needed for the same reason: whether a Mailer port exists is not
something config can be read for — a venture may hand the harness
its own adapter — so the venture answers, and a venture that
assembles Resend from the environment passes a check on its key.
Without a probe the check is skipped, exactly as for transports.
Sourcepub fn transport_probe(
self,
probe: impl Fn(&dyn Config) -> bool + Send + Sync + 'static,
) -> Self
pub fn transport_probe( self, probe: impl Fn(&dyn Config) -> bool + Send + Sync + 'static, ) -> Self
How the venture answers “is any push transport wired?”, for the
production readiness check (see TransportProbe).
A venture that assembles the Push port from the environment
passes cratefield_push_wiring::inspect_push(cfg).any_routed().
One that hands the harness its own adapter answers for itself.
Sourcepub fn vapid_public_key(
self,
probe: impl Fn(&dyn Config) -> Option<String> + Send + Sync + 'static,
) -> Self
pub fn vapid_public_key( self, probe: impl Fn(&dyn Config) -> Option<String> + Send + Sync + 'static, ) -> Self
How the venture answers “what applicationServerKey should a
browser subscribe with?”, served at GET /v1/notifications/vapid-public-key (issue #183).
The module cannot answer it, for the same reason it cannot answer
Notifications::transport_probe: the push environment has
exactly one reader, cratefield-push-wiring, and depending on it
here would pull all three push adapters into a module that must
compile the same whether a venture wires zero or three. So the
venture, which already has the reader, passes the answer in:
Notifications::new()
.vapid_public_key(cratefield::push_wiring::vapid_public_key)Without a probe — or with one that answers None, which is what
an unconfigured venture gets — the route is a 404 and cf.js
reports that this site does not offer browser push. It is the
public half of the VAPID pair, handed to every browser that
subscribes; the private half never leaves the adapter.
Sourcepub fn channel(self, channel: Arc<dyn Channel>) -> Self
pub fn channel(self, channel: Arc<dyn Channel>) -> Self
Registers one extra delivery channel (#764) — the seam a crate like a Telegram adapter implements — next to the built-in push and email:
Notifications::new().channel(Arc::new(cratefield_module_telegram::TelegramChannel::new(bot)))Call it once per channel; every registered channel queues one outbox row per notification, and the drain retries and dead-letters its failures exactly as it does mail’s. Until a channel has a preference column of its own, it follows the account’s push switch for the category, re-checked at send time. The channel resolves its own recipient out of its own tables by account id — nothing of the sort travels through the outbox payload.
The name must be lower-case letters, digits and underscores, and
registered once: validate_config refuses anything else, the same
way it refuses a duplicated category.
Sourcepub fn notifier(&self) -> Notifier
pub fn notifier(&self) -> Notifier
The handle other modules call.
Take it before composing the harness and hand it to the modules
that send notifications; it starts working when
Harness::router builds this module’s router. Taking it early is
safe in both directions: it reads the module’s finished settings
through a shared cell, so a category declared after this call is
one the handle can send.
Sourcepub fn declared_categories(&self) -> &[Category]
pub fn declared_categories(&self) -> &[Category]
The declared categories.
Trait Implementations§
Source§impl Default for Notifications
impl Default for Notifications
Source§impl Module for Notifications
impl Module for Notifications
Source§fn personal_data(&self) -> &'static [PersonalDataSet]
fn personal_data(&self) -> &'static [PersonalDataSet]
What this module holds about a person, per table (issue #244).
It held all of this before it said any of it. Erasure plans from this
list and not from tables, so until these eight
declarations existed a subject access request answered without the
stored address, the device tokens and the inbox, and an erasure
request reported success having deleted none of them.
The descriptions are published verbatim by
GET /v1/privacy/manifest and written for the person reading that
page, not for whoever maintains this file.
Two of the nine are judgements rather than the obvious answer:
notifications_subscriptionsis erased like the rest, andrecipient_jsonis redacted from the export. The row is the account’s and must go, but the column is a push token or a Web Push endpoint — a bearer capability, which whoever holds the export could notify the device with (ADR 0015).recipient_hashis exported: it identifies the device to somebody reading their own export and cannot be sent to.notifications_email_targetsis erased, including the unsubscribe record, which is the one case where erasing loses a protection. It is still right here: the address is only ever mailed because anotify()names this account, and the account’s rows are going in the same batch — so nothing remains that could reach the address, and keeping it would be keeping an address somebody asked us to forget. A venture that suppresses a bounced address is keeping that in the provider, not here.
The outbox was the third judgement until #266 gave the Outbox
shape a nullable subject column (migration 0009): until then the
queued row held the person’s message with no column a request could
match, and could only be published as unreachable. Now it is a
plain Erase set, and rows written before the migration keep
subject NULL — they drain as before and name nobody.
Source§fn self_check(&self) -> Vec<String>
fn self_check(&self) -> Vec<String>
The translation completeness check (#190).
It reads only what the venture compiled in — its catalog and the
message ids it declared — so fz doctor can run it with no
environment at all, and it says the same thing on a laptop as in
CI. The same gaps also fail Module::validate_config, so a
deployment that skipped the doctor still refuses to start.
Source§fn requires(&self) -> &'static [Port]
fn requires(&self) -> &'static [Port]
Source§fn tables(&self) -> &'static [&'static str]
fn tables(&self) -> &'static [&'static str]
Source§fn emits(&self) -> &'static [&'static str]
fn emits(&self) -> &'static [&'static str]
"<module>.<event>"), listed by
/__health.Source§fn public_writes(&self) -> bool
fn public_writes(&self) -> bool
Source§fn public_write_policy(&self) -> RoutePolicy
fn public_write_policy(&self) -> RoutePolicy
RoutePolicy on
(issue #143). Read moreSource§fn migrations(&self) -> Migrations
fn migrations(&self) -> Migrations
Source§fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>
fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>
Source§fn router(&self, ctx: ModuleContext) -> Router
fn router(&self, ctx: ModuleContext) -> Router
/v1/<name>.Source§fn events(&self) -> Vec<(EventName, EventHandler)>
fn events(&self) -> Vec<(EventName, EventHandler)>
Harness::build.Source§fn scheduled<'a>(
&'a self,
ctx: &'a ModuleContext,
cron: &'a str,
) -> BoxFuture<'a, Result<(), AnyError>>
fn scheduled<'a>( &'a self, ctx: &'a ModuleContext, cron: &'a str, ) -> BoxFuture<'a, Result<(), AnyError>>
cron is the trigger expression). Default: none.Source§fn harness_api(&self) -> u32
fn harness_api(&self) -> u32
Harness::build.Source§fn depends_on(&self) -> &'static [&'static str]
fn depends_on(&self) -> &'static [&'static str]
Source§fn actor_kinds(&self) -> &'static [&'static str]
fn actor_kinds(&self) -> &'static [&'static str]
[a-z0-9_-]; duplicates across
modules are a build error, and so is a kind with no handler registered
through
HarnessBuilder::actor. Read moreSource§fn signature_verification(&self) -> SignatureVerification
fn signature_verification(&self) -> SignatureVerification
RoutePolicy::Signature
deliveries (issue #533): how a webhook handler checks a machine
signature before acting. Read moreSource§fn max_body_bytes(&self, _cfg: &dyn Config) -> usize
fn max_body_bytes(&self, _cfg: &dyn Config) -> usize
Source§fn streaming_routes(&self) -> &'static [StreamRoute]
fn streaming_routes(&self) -> &'static [StreamRoute]
StreamRoute::max_bytes ceiling. Opt-in: the default
is empty, and every existing route keeps the buffered behaviour it
always had. Read moreSource§fn max_blob_object_bytes(&self) -> u64
fn max_blob_object_bytes(&self) -> u64
ScopedBlob the harness hands the module on
put_stream, upload_part and complete_multipart. Read moreSource§fn well_known(&self) -> Option<Router>
fn well_known(&self) -> Option<Router>
/.well-known, for
spec-mandated discovery documents (OIDC openid-configuration,
jwks.json) that must live outside /v1 (issue #46). Paths are
relative to the prefix: register /jwks.json, not
/.well-known/jwks.json. Read moreSource§fn surface(&self) -> Surface
fn surface(&self) -> Surface
Action::input::<Body>()), so the
declaration cannot drift from the route. Harness::build
validates it; GET /__surface serves the composition. Default:
nothing, and a module that declares nothing renders nothing.