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 pub fn check_signals(&self) -> bool {
56 self.mode == FilterMode::Filter && !self.signals.is_empty()
57 }
58}
59
60fn nsid_matches(pattern: &str, collection: &str) -> bool {
61 if let Some(prefix) = pattern.strip_suffix(".*") {
62 collection == prefix || collection.starts_with(prefix)
63 } else {
64 collection == pattern
65 }
66}