Skip to main content

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}