Skip to main content

Notifications

Struct Notifications 

Source
pub struct Notifications { /* private fields */ }
Expand description

Push subscriptions, preferences, fan-out and delivery.

Implementations§

Source§

impl Notifications

Source

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.

Source

pub fn category(self, category: Category) -> Self

Declares one category.

Source

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.

Source

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).

Source

pub fn drain_batch(self, rows: u64) -> Self

How many outbox rows one drain pass leases (default 50; NOTIFICATIONS_DRAIN_BATCH).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn email_window_secs(self, seconds: u32) -> Self

How long that window is, in seconds (default one hour).

Source

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.

Source

pub fn shared_catalog(self, catalog: Arc<dyn Catalog>) -> Self

A catalog the venture already holds behind an Arc — because it shares one with its own mail templates, say.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn declared_categories(&self) -> &[Category]

The declared categories.

Trait Implementations§

Source§

impl Default for Notifications

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Module for Notifications

Source§

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_subscriptions is erased like the rest, and recipient_json is 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_hash is exported: it identifies the device to somebody reading their own export and cannot be sent to.
  • notifications_email_targets is 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 a notify() 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>

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 name(&self) -> &'static str

Kebab-case name; mounted at /v1/<name>.
Source§

fn version(&self) -> &'static str

env!("CARGO_PKG_VERSION"), surfaced by /__health.
Source§

fn requires(&self) -> &'static [Port]

Ports the module cannot run without; missing = build error.
Source§

fn optional(&self) -> &'static [Port]

Ports the module uses when present.
Source§

fn tables(&self) -> &'static [&'static str]

Table names this module owns; duplicates across modules are a build error.
Source§

fn emits(&self) -> &'static [&'static str]

Event names this module emits ("<module>.<event>"), listed by /__health.
Source§

fn public_writes(&self) -> bool

Whether the module has public write endpoints. Read more
Source§

fn public_write_policy(&self) -> RoutePolicy

What actually protects those public writes, for a module with no ADR 0010 surface to hang a per-action RoutePolicy on (issue #143). Read more
Source§

fn migrations(&self) -> Migrations

The module’s migrations, embedded per dialect.
Source§

fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>

Rejects invalid configuration: missing required keys or malformed values, reported together with the module name. Read more
Source§

fn router(&self, ctx: ModuleContext) -> Router

The module’s router, nested under /v1/<name>.
Source§

fn events(&self) -> Vec<(EventName, EventHandler)>

Handlers for events other modules emit; registered at Harness::build.
Source§

fn scheduled<'a>( &'a self, ctx: &'a ModuleContext, cron: &'a str, ) -> BoxFuture<'a, Result<(), AnyError>>

Scheduled work (cron is the trigger expression). Default: none.
Source§

fn harness_api(&self) -> u32

Contract version, checked by Harness::build.
Source§

fn depends_on(&self) -> &'static [&'static str]

Modules this one’s schema sits on top of, by name. Read more
Source§

fn actor_kinds(&self) -> &'static [&'static str]

Actor kinds this module owns, each [a-z0-9_-]; duplicates across modules are a build error, and so is a kind with no handler registered through HarnessBuilder::actor. Read more
Source§

fn signature_verification(&self) -> SignatureVerification

Which verifier proves this module’s RoutePolicy::Signature deliveries (issue #533): how a webhook handler checks a machine signature before acting. Read more
Source§

fn max_body_bytes(&self, _cfg: &dyn Config) -> usize

The largest request body any route of this module accepts, so a runtime can refuse an oversize body before buffering it (issue #440). Read more
Source§

fn streaming_routes(&self) -> &'static [StreamRoute]

Routes this module serves in streaming mode (issue #585), each with its own StreamRoute::max_bytes ceiling. Opt-in: the default is empty, and every existing route keeps the buffered behaviour it always had. Read more
Source§

fn max_blob_object_bytes(&self) -> u64

The largest object this module’s streamed and multipart blob writes may produce (issue #586), enforced by the ScopedBlob the harness hands the module on put_stream, upload_part and complete_multipart. Read more
Source§

fn well_known(&self) -> Option<Router>

Routes this module serves at the root under /.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 more
Source§

fn surface(&self) -> Surface

The module’s UI surface (ADR 0010): the actions a renderer may offer and the views that compose them. Input schemas come from the handler’s own body types (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.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more