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
//! Provider-neutral push delivery with an Expo adapter.
//!
//! The crate separates a small [`PushSender`] port from its [`ExpoPushSender`]
//! adapter. Domain code builds [`PushMessage`] values and reads [`PushOutcome`]
//! values; nothing above the port names a provider. Scheduling, quiet hours,
//! and deciding who gets notified stay in the product.
//!
//! # Two-phase delivery
//!
//! Expo answers a send with a *ticket* per notification, then confirms delivery
//! later through a *receipt*. [`ExpoPushSender`] runs both phases per batch, so
//! one call returns [`PushDeliveryStatus::Delivered`] or
//! [`PushDeliveryStatus::Rejected`] wherever Expo has settled, and
//! [`PushDeliveryStatus::Accepted`] where it has not. Never resend on
//! `Accepted`; the notification is in flight.
//!
//! # Deferred receipts
//!
//! An `Accepted` outcome carries the provider's [`PushTicketId`]. Record those
//! with [`PendingReceiptStore::record_accepted`] and let the product's job
//! runner call [`poll_pending_receipts`], which reads the receipts through the
//! [`PushReceiptSource`] port and invalidates a `DeviceNotRegistered` token
//! that Expo only reported after the send. The crate ships no scheduler.
//!
//! # Device registry
//!
//! A device token stops working once the app is uninstalled. Expo reports that
//! as `DeviceNotRegistered`, which becomes
//! [`PushRejection::DeviceNotRegistered`]. The [`DeviceRegistry`] port stores
//! each owner's tokens and removes the dead ones in one call after a send:
//!
//! ```rust
//! use baukit_push::{DeviceRegistry, PushMessage, PushSender};
//! use chrono::Utc;
//!
//! async fn deliver(
//! sender: &impl PushSender,
//! registry: &impl DeviceRegistry,
//! messages: Vec<PushMessage>,
//! ) -> Result<u64, Box<dyn std::error::Error>> {
//! let sent_at = Utc::now();
//! let outcomes = sender.send(messages).await?;
//! Ok(registry.invalidate_dead_tokens(&outcomes, sent_at).await?)
//! }
//! ```
//!
//! The registry also registers, rotates, unregisters, lists, and erases
//! tokens, and caps devices per owner by evicting the least recently
//! registered. The optional `sqlx-postgres` feature adds
//! `PostgresDeviceRegistry` over the schema in
//! [`POSTGRES_PUSH_DEVICES_MIGRATION_SQL`]. [`DeviceToken`] redacts itself in
//! `Debug`, so [`PushMessage`] and [`PushOutcome`] are safe to log, and no
//! error carries a token.
//!
//! # Daily delivery claims
//!
//! A scheduled sender claims a [`DeliveryClaim`] keyed by owner, local date,
//! and kind before it sends, and releases it if the send fails, so two workers
//! never deliver the same daily notification twice. The [`DeliveryClaimStore`]
//! port is separate from the registry; event-driven senders skip it.
//! Eligibility, copy, quiet hours, and channel choice stay in the product.
//!
//! # Retries
//!
//! Whole-request failures arrive as [`PushError::Transport`] carrying a
//! [`baukit_http::RetryClass`], so an Expo rate limit with a `Retry-After`
//! header reaches the caller as a concrete delay through
//! [`PushError::retry_after`]. Per-notification refusals are not errors:
//! inspect [`PushRejection::is_retryable`] on each outcome instead.
pub use ;
pub use ;
pub use ExpoPushSender;
pub use FakePushSender;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
/// Reference PostgreSQL schema for the `sqlx-postgres` feature's `PostgresDeviceRegistry`.
///
/// Copy this SQL into a product migration; do not execute it dynamically during
/// application startup. Then add the product's owner foreign key with
/// `ON DELETE CASCADE`, as the file's header describes.
pub const POSTGRES_PUSH_DEVICES_MIGRATION_SQL: &str =
include_str!;
/// Reference PostgreSQL schema for the `sqlx-postgres` feature's `PostgresDeliveryClaimStore`.
///
/// Only products that send scheduled pushes need it. Copy it into a product
/// migration and add the owner foreign key the file's header describes.
pub const POSTGRES_PUSH_DELIVERY_CLAIMS_MIGRATION_SQL: &str =
include_str!;
/// Reference PostgreSQL schema for the `sqlx-postgres` feature's `PostgresPendingReceiptStore`.
///
/// Only products that poll receipts after the send need it. Copy it into a
/// product migration; it has no owner column.
pub const POSTGRES_PUSH_PENDING_RECEIPTS_MIGRATION_SQL: &str =
include_str!;
// Compiles the README's examples so they cannot drift from the API.
;