navi_notifier_core/traits.rs
1//! The extension seams that make navi provider-agnostic.
2//!
3//! A provider crate implements [`Source`] (things that produce events) and/or
4//! [`Notifier`] (things that deliver them). The engine wires arbitrary sets of each
5//! together through the registry, so adding GitLab or Discord is "implement a trait,
6//! register a constructor" — no engine changes.
7
8use async_trait::async_trait;
9
10use crate::error::{NotifyError, SourceError, StateError};
11use crate::model::Event;
12
13/// Durable, provider-agnostic storage the engine and sources rely on.
14///
15/// Three responsibilities:
16/// - **Snapshots**: opaque per-PR bytes a source uses to diff current vs. last-seen
17/// state. The store neither interprets nor validates them.
18/// - **Dedup**: a set of delivered `dedup_key`s guaranteeing idempotent delivery.
19/// - **Cursors**: small opaque strings for poll bookkeeping (ETags, timestamps).
20#[async_trait]
21pub trait StateStore: Send + Sync {
22 async fn get_snapshot(
23 &self,
24 source_id: &str,
25 scope: &str,
26 ) -> Result<Option<Vec<u8>>, StateError>;
27
28 async fn put_snapshot(
29 &self,
30 source_id: &str,
31 scope: &str,
32 bytes: &[u8],
33 ) -> Result<(), StateError>;
34
35 /// True if `dedup_key` was already delivered successfully.
36 async fn was_delivered(&self, dedup_key: &str) -> Result<bool, StateError>;
37
38 /// Record `dedup_key` as delivered. Idempotent.
39 async fn mark_delivered(&self, dedup_key: &str) -> Result<(), StateError>;
40
41 async fn get_cursor(&self, source_id: &str, key: &str) -> Result<Option<String>, StateError>;
42
43 async fn put_cursor(&self, source_id: &str, key: &str, value: &str) -> Result<(), StateError>;
44}
45
46/// A producer of normalized [`Event`]s.
47///
48/// `poll` is expected to (1) read prior snapshots/cursors from `state`, (2) fetch
49/// current provider state, (3) diff to derive events, and (4) persist advanced
50/// snapshots/cursors back to `state`. Idempotent *delivery* is the engine's job via
51/// the dedup set, so `poll` may legitimately return events it has returned before;
52/// the engine filters them out.
53#[async_trait]
54pub trait Source: Send + Sync {
55 /// Stable identifier, e.g. `"github"`. Used in dedup keys and config routing.
56 fn id(&self) -> &str;
57
58 /// Poll the provider and return newly-derived events (unordered).
59 async fn poll(&self, state: &dyn StateStore) -> Result<Vec<Event>, SourceError>;
60
61 /// Optional hook invoked once an event has been delivered successfully, letting
62 /// the source advance provider-side state (e.g. mark a notification thread read).
63 /// Default: no-op.
64 async fn commit(&self, _state: &dyn StateStore, _event: &Event) -> Result<(), SourceError> {
65 Ok(())
66 }
67}
68
69/// A delivery target for events (Slack today; Discord, email, … later).
70#[async_trait]
71pub trait Notifier: Send + Sync {
72 /// Stable identifier, e.g. `"slack"`.
73 fn id(&self) -> &str;
74
75 /// Deliver a single, already-filtered event. Implementations should be
76 /// resilient to transient failure (retry/backoff) before returning `Err`.
77 async fn send(&self, event: &Event) -> Result<(), NotifyError>;
78}