at protocol indexer with flexible filtering, xrpc queries, and a cursor-backed event stream, built on fjall
at-protocol
atproto
indexer
rust
fjall
1use jacquard_common::types::nsid::Nsid;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4
5pub type FilterHandle = Arc<arc_swap::ArcSwap<FilterConfig>>;
6
7pub fn new_handle(config: FilterConfig) -> FilterHandle {
8 Arc::new(arc_swap::ArcSwap::new(Arc::new(config)))
9}
10
11/// apply a bool patch or set replacement for a single set update.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum SetUpdate {
15 /// replace the entire set with this list
16 Set(Vec<String>),
17 /// patch: true = add, false = remove
18 Patch(std::collections::HashMap<String, bool>),
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum FilterMode {
24 Filter = 0,
25 Full = 2,
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct FilterConfig {
30 pub mode: FilterMode,
31 pub signals: Vec<Nsid<'static>>,
32 pub collections: Vec<Nsid<'static>>,
33}
34
35impl FilterConfig {
36 pub fn new(mode: FilterMode) -> Self {
37 Self {
38 mode,
39 signals: Vec::new(),
40 collections: Vec::new(),
41 }
42 }
43
44 pub fn matches_collection(&self, collection: &str) -> bool {
45 if self.collections.is_empty() {
46 return true;
47 }
48 self.collections.iter().any(|p| nsid_matches(p, collection))
49 }
50
51 pub fn matches_signal(&self, collection: &str) -> bool {
52 self.signals.iter().any(|p| nsid_matches(p, collection))
53 }
54
55 fn has_glob_signals(&self) -> bool {
56 self.signals.iter().any(|s| s.ends_with(".*"))
57 }
58
59 pub fn check_signals(&self) -> bool {
60 self.mode == FilterMode::Filter && !self.signals.is_empty() && !self.has_glob_signals()
61 }
62}
63
64fn nsid_matches(pattern: &str, collection: &str) -> bool {
65 if let Some(prefix) = pattern.strip_suffix(".*") {
66 collection == prefix || collection.starts_with(prefix)
67 } else {
68 collection == pattern
69 }
70}