···7use clap::Parser;
8use jetstream::{
9 events::{
10- commit::{
11- CommitEvent,
12- CommitType,
13- },
14- JetstreamEvent::Commit,
15 },
16 DefaultJetstreamEndpoints,
17 JetstreamCompression,
···25 /// The DIDs to listen for events on, if not provided we will listen for all DIDs.
26 #[arg(short, long)]
27 did: Option<Vec<string::Did>>,
28- /// The NSID for the collection to listen for (e.g. `app.bsky.feed.post`).
29- #[arg(short, long)]
30- nsid: string::Nsid,
31}
3233#[tokio::main]
···37 let dids = args.did.unwrap_or_default();
38 let config = JetstreamConfig {
39 endpoint: DefaultJetstreamEndpoints::USEastOne.into(),
40- wanted_collections: vec![args.nsid.clone()],
41 wanted_dids: dids.clone(),
42 compression: JetstreamCompression::Zstd,
43 ..Default::default()
···46 let jetstream = JetstreamConnector::new(config)?;
47 let mut receiver = jetstream.connect().await?;
4849- println!(
50- "Listening for '{}' events on DIDs: {:?}",
51- args.nsid.as_str(),
52- dids
53- );
5455 while let Some(event) = receiver.recv().await {
56- if let Commit(commit) = event {
57- match commit {
58- CommitEvent::CreateOrUpdate { info: _, commit }
59- if commit.info.operation == CommitType::Create =>
60- {
61- if let AppBskyFeedPost(record) = commit.record {
62- println!(
63- "New post created! ({})\n\n'{}'",
64- commit.info.rkey.as_str(),
65- record.text
66- );
67- }
68- }
69- CommitEvent::Delete { info: _, commit } => {
70- println!("A post has been deleted. ({})", commit.rkey.as_str());
71- }
72- _ => {}
73 }
74 }
75 }
···7use clap::Parser;
8use jetstream::{
9 events::{
10+ CommitEvent,
11+ CommitOp,
12+ EventKind,
13+ JetstreamEvent,
014 },
15 DefaultJetstreamEndpoints,
16 JetstreamCompression,
···24 /// The DIDs to listen for events on, if not provided we will listen for all DIDs.
25 #[arg(short, long)]
26 did: Option<Vec<string::Did>>,
00027}
2829#[tokio::main]
···33 let dids = args.did.unwrap_or_default();
34 let config = JetstreamConfig {
35 endpoint: DefaultJetstreamEndpoints::USEastOne.into(),
36+ wanted_collections: vec![string::Nsid::new("app.bsky.feed.post".to_string()).unwrap()],
37 wanted_dids: dids.clone(),
38 compression: JetstreamCompression::Zstd,
39 ..Default::default()
···42 let jetstream = JetstreamConnector::new(config)?;
43 let mut receiver = jetstream.connect().await?;
4445+ println!("Listening for 'app.bsky.feed.post' events on DIDs: {dids:?}");
00004647 while let Some(event) = receiver.recv().await {
48+ if let JetstreamEvent {
49+ kind: EventKind::Commit,
50+ commit:
51+ Some(CommitEvent {
52+ operation: CommitOp::Create,
53+ rkey,
54+ record: Some(record),
55+ ..
56+ }),
57+ ..
58+ } = event
59+ {
60+ if let Ok(AppBskyFeedPost(rec)) = serde_json::from_str(record.get()) {
61+ println!("New post created! ({})\n{:?}\n", rkey.as_str(), rec.text);
00062 }
63 }
64 }
-40
jetstream/src/events/account.rs
···1-use chrono::Utc;
2-use serde::Deserialize;
3-4-use crate::{
5- events::EventInfo,
6- exports,
7-};
8-9-/// An event representing a change to an account.
10-#[derive(Deserialize, Debug)]
11-pub struct AccountEvent {
12- /// Basic metadata included with every event.
13- #[serde(flatten)]
14- pub info: EventInfo,
15- /// Account specific data bundled with this event.
16- pub account: AccountData,
17-}
18-19-/// Account specific data bundled with an account event.
20-#[derive(Deserialize, Debug)]
21-pub struct AccountData {
22- /// Whether the account is currently active.
23- pub active: bool,
24- /// The DID of the account.
25- pub did: exports::Did,
26- pub seq: u64,
27- pub time: chrono::DateTime<Utc>,
28- /// If `active` is `false` this will be present to explain why the account is inactive.
29- pub status: Option<AccountStatus>,
30-}
31-32-/// The possible reasons an account might be listed as inactive.
33-#[derive(Deserialize, Debug)]
34-#[serde(rename_all = "lowercase")]
35-pub enum AccountStatus {
36- Deactivated,
37- Deleted,
38- Suspended,
39- TakenDown,
40-}
···0000000000000000000000000000000000000000
-55
jetstream/src/events/commit.rs
···1-use serde::Deserialize;
2-3-use crate::{
4- events::EventInfo,
5- exports,
6-};
7-8-/// An event representing a repo commit, which can be a `create`, `update`, or `delete` operation.
9-#[derive(Deserialize, Debug)]
10-#[serde(untagged, rename_all = "snake_case")]
11-pub enum CommitEvent<R> {
12- CreateOrUpdate {
13- #[serde(flatten)]
14- info: EventInfo,
15- commit: CommitData<R>,
16- },
17- Delete {
18- #[serde(flatten)]
19- info: EventInfo,
20- commit: CommitInfo,
21- },
22-}
23-24-/// The type of commit operation that was performed.
25-#[derive(Deserialize, Debug, PartialEq)]
26-#[serde(rename_all = "snake_case")]
27-pub enum CommitType {
28- Create,
29- Update,
30- Delete,
31-}
32-33-/// Basic commit specific info bundled with every event, also the only data included with a `delete`
34-/// operation.
35-#[derive(Deserialize, Debug)]
36-pub struct CommitInfo {
37- /// The type of commit operation that was performed.
38- pub operation: CommitType,
39- pub rev: String,
40- pub rkey: exports::RecordKey,
41- /// The NSID of the record type that this commit is associated with.
42- pub collection: exports::Nsid,
43-}
44-45-/// Detailed data bundled with a commit event. This data is only included when the event is
46-/// `create` or `update`.
47-#[derive(Deserialize, Debug)]
48-pub struct CommitData<R> {
49- #[serde(flatten)]
50- pub info: CommitInfo,
51- /// The CID of the record that was operated on.
52- pub cid: exports::Cid,
53- /// The record that was operated on.
54- pub record: R,
55-}
···1-use chrono::Utc;
2-use serde::Deserialize;
3-4-use crate::{
5- events::EventInfo,
6- exports,
7-};
8-9-/// An event representing a change to an identity.
10-#[derive(Deserialize, Debug)]
11-pub struct IdentityEvent {
12- /// Basic metadata included with every event.
13- #[serde(flatten)]
14- pub info: EventInfo,
15- /// Identity specific data bundled with this event.
16- pub identity: IdentityData,
17-}
18-19-/// Identity specific data bundled with an identity event.
20-#[derive(Deserialize, Debug)]
21-pub struct IdentityData {
22- /// The DID of the identity.
23- pub did: exports::Did,
24- /// The handle associated with the identity.
25- pub handle: Option<exports::Handle>,
26- pub seq: u64,
27- pub time: chrono::DateTime<Utc>,
28-}
···0000000000000000000000000000
+90-32
jetstream/src/events/mod.rs
···1-pub mod account;
2-pub mod commit;
3-pub mod identity;
4-5use std::time::{
6 Duration,
7 SystemTime,
···9 UNIX_EPOCH,
10};
11012use serde::Deserialize;
01314use crate::exports;
1516/// Opaque wrapper for the time_us cursor used by jetstream
17-///
18-/// Generally, you should use a cursor
19#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd)]
20pub struct Cursor(u64);
2122-/// Basic data that is included with every event.
23-#[derive(Deserialize, Debug)]
24-pub struct EventInfo {
0025 pub did: exports::Did,
26- pub time_us: Cursor,
27 pub kind: EventKind,
00028}
2930-#[derive(Deserialize, Debug)]
31-#[serde(untagged)]
32-pub enum JetstreamEvent<R> {
33- Commit(commit::CommitEvent<R>),
34- Identity(identity::IdentityEvent),
35- Account(account::AccountEvent),
36-}
37-38-#[derive(Deserialize, Debug)]
39#[serde(rename_all = "snake_case")]
40pub enum EventKind {
41 Commit,
···43 Account,
44}
4546-impl<R> JetstreamEvent<R> {
47- pub fn cursor(&self) -> Cursor {
48- match self {
49- JetstreamEvent::Commit(commit::CommitEvent::CreateOrUpdate { info, .. }) => {
50- info.time_us.clone()
51- }
52- JetstreamEvent::Commit(commit::CommitEvent::Delete { info, .. }) => {
53- info.time_us.clone()
54- }
55- JetstreamEvent::Identity(e) => e.info.time_us.clone(),
56- JetstreamEvent::Account(e) => e.info.time_us.clone(),
57- }
58- }
00000000000000000000059}
6061impl Cursor {
···136 UNIX_EPOCH + Duration::from_micros(c.0)
137 }
138}
000000000000000000000000000000000000000000000
···7 Cursor as IoCursor,
8 Read,
9 },
10- marker::PhantomData,
11 time::{
12 Duration,
13 Instant,
14 },
15};
1617-use atrium_api::record::KnownRecord;
18use futures_util::{
19 stream::StreamExt,
20 SinkExt,
21};
22-use serde::de::DeserializeOwned;
23use tokio::{
24 net::TcpStream,
25 sync::mpsc::{
···124const JETSTREAM_ZSTD_DICTIONARY: &[u8] = include_bytes!("../zstd/dictionary");
125126/// A receiver channel for consuming Jetstream events.
127-pub type JetstreamReceiver<R> = Receiver<JetstreamEvent<R>>;
128129/// An internal sender channel for sending Jetstream events to [JetstreamReceiver]'s.
130-type JetstreamSender<R> = Sender<JetstreamEvent<R>>;
131132/// A wrapper connector type for working with a WebSocket connection to a Jetstream instance to
133/// receive and consume events. See [JetstreamConnector::connect] for more info.
134-pub struct JetstreamConnector<R: DeserializeOwned> {
135 /// The configuration for the Jetstream connection.
136- config: JetstreamConfig<R>,
137}
138139pub enum JetstreamCompression {
···163 }
164}
165166-pub struct JetstreamConfig<R: DeserializeOwned = KnownRecord> {
167 /// A Jetstream endpoint to connect to with a WebSocket Scheme i.e.
168 /// `wss://jetstream1.us-east.bsky.network/subscribe`.
169 pub endpoint: String,
···200 /// can help prevent that if your consumer sometimes pauses, at a cost of higher memory
201 /// usage while events are buffered.
202 pub channel_size: usize,
203- /// Marker for record deserializable type.
204- ///
205- /// See examples/arbitrary_record.rs for an example using serde_json::Value
206- ///
207- /// You can omit this if you construct `JetstreamConfig { a: b, ..Default::default() }.
208- /// If you have to specify it, use `std::marker::PhantomData` with no type parameters.
209- pub record_type: PhantomData<R>,
210}
211212-impl<R: DeserializeOwned> Default for JetstreamConfig<R> {
213 fn default() -> Self {
214 JetstreamConfig {
215 endpoint: DefaultJetstreamEndpoints::USEastOne.into(),
···220 omit_user_agent_jetstream_info: false,
221 replay_on_reconnect: false,
222 channel_size: 4096, // a few seconds of firehose buffer
223- record_type: PhantomData,
224 }
225 }
226}
227228-impl<R: DeserializeOwned> JetstreamConfig<R> {
229 /// Constructs a new endpoint URL with the given [JetstreamConfig] applied.
230 pub fn get_request_builder(
231 &self,
···313 }
314}
315316-impl<R: DeserializeOwned + Send + 'static> JetstreamConnector<R> {
317 /// Create a Jetstream connector with a valid [JetstreamConfig].
318 ///
319 /// After creation, you can call [connect] to connect to the provided Jetstream instance.
320- pub fn new(config: JetstreamConfig<R>) -> Result<Self, ConfigValidationError> {
321 // We validate the configuration here so any issues are caught early.
322 config.validate()?;
323 Ok(JetstreamConnector { config })
···327 ///
328 /// A [JetstreamReceiver] is returned which can be used to respond to events. When all instances
329 /// of this receiver are dropped, the connection and task are automatically closed.
330- pub async fn connect(&self) -> Result<JetstreamReceiver<R>, ConnectionError> {
331 self.connect_cursor(None).await
332 }
333···343 pub async fn connect_cursor(
344 &self,
345 cursor: Option<Cursor>,
346- ) -> Result<JetstreamReceiver<R>, ConnectionError> {
347 // We validate the config again for good measure. Probably not necessary but it can't hurt.
348 self.config
349 .validate()
···424425/// The main task that handles the WebSocket connection and sends [JetstreamEvent]'s to any
426/// receivers that are listening for them.
427-async fn websocket_task<R: DeserializeOwned>(
428 dictionary: DecoderDictionary<'_>,
429 ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
430- send_channel: JetstreamSender<R>,
431 last_cursor: &mut Option<Cursor>,
432) -> Result<(), JetstreamEventError> {
433 // TODO: Use the write half to allow the user to change configuration settings on the fly.
···439 Some(Ok(message)) => {
440 match message {
441 Message::Text(json) => {
442- let event: JetstreamEvent<R> = serde_json::from_str(&json)
443 .map_err(JetstreamEventError::ReceivedMalformedJSON)?;
444- let event_cursor = event.cursor();
445446 if let Some(last) = last_cursor {
447 if event_cursor <= *last {
···475 .read_to_string(&mut json)
476 .map_err(JetstreamEventError::CompressionDecoderError)?;
477478- let event: JetstreamEvent<R> = serde_json::from_str(&json)
479- .map_err(JetstreamEventError::ReceivedMalformedJSON)?;
480- let event_cursor = event.cursor();
00481482 if let Some(last) = last_cursor {
483 if event_cursor <= *last {
···7 Cursor as IoCursor,
8 Read,
9 },
010 time::{
11 Duration,
12 Instant,
13 },
14};
15016use futures_util::{
17 stream::StreamExt,
18 SinkExt,
19};
020use tokio::{
21 net::TcpStream,
22 sync::mpsc::{
···121const JETSTREAM_ZSTD_DICTIONARY: &[u8] = include_bytes!("../zstd/dictionary");
122123/// A receiver channel for consuming Jetstream events.
124+pub type JetstreamReceiver = Receiver<JetstreamEvent>;
125126/// An internal sender channel for sending Jetstream events to [JetstreamReceiver]'s.
127+type JetstreamSender = Sender<JetstreamEvent>;
128129/// A wrapper connector type for working with a WebSocket connection to a Jetstream instance to
130/// receive and consume events. See [JetstreamConnector::connect] for more info.
131+pub struct JetstreamConnector {
132 /// The configuration for the Jetstream connection.
133+ config: JetstreamConfig,
134}
135136pub enum JetstreamCompression {
···160 }
161}
162163+pub struct JetstreamConfig {
164 /// A Jetstream endpoint to connect to with a WebSocket Scheme i.e.
165 /// `wss://jetstream1.us-east.bsky.network/subscribe`.
166 pub endpoint: String,
···197 /// can help prevent that if your consumer sometimes pauses, at a cost of higher memory
198 /// usage while events are buffered.
199 pub channel_size: usize,
0000000200}
201202+impl Default for JetstreamConfig {
203 fn default() -> Self {
204 JetstreamConfig {
205 endpoint: DefaultJetstreamEndpoints::USEastOne.into(),
···210 omit_user_agent_jetstream_info: false,
211 replay_on_reconnect: false,
212 channel_size: 4096, // a few seconds of firehose buffer
0213 }
214 }
215}
216217+impl JetstreamConfig {
218 /// Constructs a new endpoint URL with the given [JetstreamConfig] applied.
219 pub fn get_request_builder(
220 &self,
···302 }
303}
304305+impl JetstreamConnector {
306 /// Create a Jetstream connector with a valid [JetstreamConfig].
307 ///
308 /// After creation, you can call [connect] to connect to the provided Jetstream instance.
309+ pub fn new(config: JetstreamConfig) -> Result<Self, ConfigValidationError> {
310 // We validate the configuration here so any issues are caught early.
311 config.validate()?;
312 Ok(JetstreamConnector { config })
···316 ///
317 /// A [JetstreamReceiver] is returned which can be used to respond to events. When all instances
318 /// of this receiver are dropped, the connection and task are automatically closed.
319+ pub async fn connect(&self) -> Result<JetstreamReceiver, ConnectionError> {
320 self.connect_cursor(None).await
321 }
322···332 pub async fn connect_cursor(
333 &self,
334 cursor: Option<Cursor>,
335+ ) -> Result<JetstreamReceiver, ConnectionError> {
336 // We validate the config again for good measure. Probably not necessary but it can't hurt.
337 self.config
338 .validate()
···413414/// The main task that handles the WebSocket connection and sends [JetstreamEvent]'s to any
415/// receivers that are listening for them.
416+async fn websocket_task(
417 dictionary: DecoderDictionary<'_>,
418 ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
419+ send_channel: JetstreamSender,
420 last_cursor: &mut Option<Cursor>,
421) -> Result<(), JetstreamEventError> {
422 // TODO: Use the write half to allow the user to change configuration settings on the fly.
···428 Some(Ok(message)) => {
429 match message {
430 Message::Text(json) => {
431+ let event: JetstreamEvent = serde_json::from_str(&json)
432 .map_err(JetstreamEventError::ReceivedMalformedJSON)?;
433+ let event_cursor = event.cursor.clone();
434435 if let Some(last) = last_cursor {
436 if event_cursor <= *last {
···464 .read_to_string(&mut json)
465 .map_err(JetstreamEventError::CompressionDecoderError)?;
466467+ let event: JetstreamEvent = serde_json::from_str(&json).map_err(|e| {
468+ eprintln!("lkasjdflkajsd {e:?} {json}");
469+ JetstreamEventError::ReceivedMalformedJSON(e)
470+ })?;
471+ let event_cursor = event.cursor.clone();
472473 if let Some(last) = last_cursor {
474 if event_cursor <= *last {
···1pub mod consumer;
2pub mod db_types;
3pub mod server;
4-pub mod store;
05pub mod store_types;
67use jetstream::events::Cursor;
···1pub mod consumer;
2pub mod db_types;
3pub mod server;
4+// pub mod storage;
5+pub mod storage_fjall;
6pub mod store_types;
78use jetstream::events::Cursor;
+2-2
ufos/src/main.rs
···1use clap::Parser;
2use std::path::PathBuf;
3-use ufos::{consumer, server, store};
45#[cfg(not(target_env = "msvc"))]
6use tikv_jemallocator::Jemalloc;
···4344 let args = Args::parse();
45 let (storage, cursor) =
46- store::Storage::open(args.data, &args.jetstream, args.jetstream_force).await?;
4748 println!("starting server with storage...");
49 let serving = server::serve(storage.clone());
···1use clap::Parser;
2use std::path::PathBuf;
3+use ufos::{consumer, server, storage_fjall};
45#[cfg(not(target_env = "msvc"))]
6use tikv_jemallocator::Jemalloc;
···4344 let args = Args::parse();
45 let (storage, cursor) =
46+ storage_fjall::Storage::open(args.data, &args.jetstream, args.jetstream_force).await?;
4748 println!("starting server with storage...");
49 let serving = server::serve(storage.clone());