reliar_outbox/enqueue.rs
1//! The enqueue capability an application calls directly, in its own transaction — decision #37,
2//! ADR 0036 amendment B: there is no facade type between the caller and the store.
3
4use reliar_core::{Classify, Envelope, Message, MessageId};
5
6/// Enqueuing a typed message in the caller's own transaction. A provider (`PostgresOutboxStore`)
7/// implements this directly, alongside [`crate::OutboxStore`]; there is no separate handle type
8/// to construct.
9///
10/// `Tx` is the provider's transaction type: `sqlx::Transaction<'_, Postgres>` for
11/// `reliar-store-postgres`. It is a **type parameter** precisely so this crate names no storage
12/// type (SRS §19.6), and one implementor may support several.
13///
14/// Deliberately **not** a method on [`crate::OutboxStore`]: enqueuing takes a transaction handle
15/// the claim side never sees, `OutboxStore` is already published, and a GAT `type Tx<'a>` would
16/// have to spell `&'a mut Transaction<'c, _>` and reintroduce an invariance problem.
17///
18/// **Two methods, one call each** (decision #42, ADR 0037 amendment A, shape D): a required
19/// [`Self::enqueue_envelope`] that every implementor writes, and a provided [`Self::enqueue`] —
20/// the fire-and-forget spelling — built on top of it. Earlier shapes tried an `impl
21/// Into<Envelope<T>>` parameter (shape B) so one method covered both a bare `T: Message` and an
22/// already-built `Envelope<T>`; the human rejected it (decision #42) for a second, explicit
23/// method instead — no inference trick, no `Envelope<T>: !Message` invariant to guard.
24///
25/// **One serialized twin, deliberately absent** (decision #38, ADR 0036 amendment B.10): a
26/// `enqueue_serialized(&mut tx, &SerializedEnvelope)` existed briefly and was cut for having no
27/// production caller; re-adding it is additive.
28///
29/// Renamed from `OutboxStaging` in 0.4.0 (decision #34); `stage` became `enqueue`, and the
30/// facade `OutboxPublisher` that briefly wrapped it (0.4.0, never released) was withdrawn before
31/// shipping in favor of calling this trait directly (decision #37).
32pub trait OutboxEnqueue<Tx>: Send + Sync {
33 /// What enqueuing fails with.
34 type Error: std::error::Error + Send + Sync + 'static + Classify;
35
36 /// Serializes `envelope`'s body with **this implementor's own** configured `Serializer` and
37 /// enqueues it in `tx`. Writes the serializer's own `content_type`.
38 ///
39 /// The **propagating** spelling: use it when an id must carry over from an inbound request
40 /// — conversation, correlation, causation, tenant, trace, headers — via
41 /// [`Envelope::builder`](reliar_core::Envelope::builder):
42 ///
43 /// ```ignore
44 /// store.enqueue_envelope(&mut tx, Envelope::builder(evt).conversation(cx).build()).await?;
45 /// ```
46 ///
47 /// Returns the id written, for the caller's own use — e.g. as a *next* message's
48 /// `causation_id` in the same transaction. The envelope already carries its `id`; this is not
49 /// how a caller learns it, only a convenience.
50 ///
51 /// **Implementors:** the trait bounds neither `T` nor `Tx` on `Send`, so a plain `async fn`
52 /// that carries `envelope: Envelope<T>` (or `tx`) across an `.await` will not satisfy this
53 /// method's `+ Send` return bound. Serialize (or otherwise consume) `T` synchronously, before
54 /// the async block is built — see `PostgresOutboxStore::enqueue_envelope`'s `//` comment in
55 /// `reliar-store-postgres` for the reference shape and the full argument.
56 ///
57 /// The implementation SHALL issue no network I/O other than the statement itself, and SHALL
58 /// NOT commit, roll back or otherwise consume `tx` — the caller owns it.
59 ///
60 /// # Errors
61 ///
62 /// Provider-defined. An `Err` **MAY** leave `tx` unusable, and whether it does is the
63 /// provider's contract — every implementor documents which. The portable rule a caller can
64 /// rely on is therefore: treat any enqueue error as *abort this transaction* — issue no
65 /// further statement on `tx`, roll it back, and consider every earlier write in it lost. With
66 /// `reliar-store-postgres` the transaction **is** aborted: PostgreSQL rejects every
67 /// subsequent statement on it, so no earlier write in that transaction can still be committed.
68 fn enqueue_envelope<T: Message + Sync>(
69 &self,
70 tx: &mut Tx,
71 envelope: Envelope<T>,
72 ) -> impl Future<Output = Result<MessageId, Self::Error>> + Send;
73
74 /// The **fire-and-forget** spelling: builds `body` into an envelope with default metadata
75 /// and a freshly rooted conversation — exactly `Envelope::builder(body).build()` — then
76 /// enqueues it via [`Self::enqueue_envelope`]:
77 ///
78 /// ```ignore
79 /// store.enqueue(&mut tx, OrderCreated { order_id }).await?;
80 /// ```
81 ///
82 /// Use [`Self::enqueue_envelope`] instead when an id must propagate from an inbound request;
83 /// this spelling never has anything to propagate from.
84 ///
85 /// Implementors **SHALL NOT** override this method — it is a fixed, provided spelling of
86 /// [`Self::enqueue_envelope`], not an extension point. `Envelope::builder(body).build()` mints
87 /// the envelope and its id eagerly, at call time, not on the returned future's first poll.
88 ///
89 /// # Errors
90 ///
91 /// Same as [`Self::enqueue_envelope`].
92 // Direct delegation, not `async fn` and no `async move` block either — returns
93 // `enqueue_envelope`'s own future unchanged. See `PostgresOutboxStore::enqueue_envelope`'s
94 // comment in `reliar-store-postgres/src/store.rs` for the full `Send`/capture argument.
95 fn enqueue<T: Message + Sync>(
96 &self,
97 tx: &mut Tx,
98 body: T,
99 ) -> impl Future<Output = Result<MessageId, Self::Error>> + Send {
100 self.enqueue_envelope(tx, Envelope::builder(body).build())
101 }
102}