Skip to main content

p2panda_sync/manager/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Manager for initiating and orchestrating topic log sync sessions.
4//!
5//! Concurrently running sessions perform message forwarding with de-duplication. Events from all
6//! running sync sessions can be consumed via a single manager event stream.
7mod event_stream;
8mod session_map;
9
10pub use event_stream::ManagerEventStream;
11use p2panda_store::topics::TopicStore;
12pub use session_map::SessionTopicMap;
13
14use std::collections::HashMap;
15use std::fmt::Debug;
16use std::hash::Hash as StdHash;
17use std::marker::PhantomData;
18use std::pin::Pin;
19
20use futures::channel::mpsc;
21use futures::future::ready;
22use futures::stream::SelectAll;
23use futures::{Sink, SinkExt, Stream, StreamExt};
24use p2panda_core::{Extensions, Hash, LogId, Operation, VerifyingKey};
25use p2panda_store::logs::LogStore;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tokio::sync::broadcast;
29use tokio_stream::wrappers::BroadcastStream;
30use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
31use tracing::debug;
32
33use crate::dedup::DeduplicationBuffer;
34use crate::manager::event_stream::{ManagerEventStreamState, StreamDebug};
35use crate::protocols::{TopicLogSync, TopicLogSyncError, TopicLogSyncEvent};
36use crate::traits::Manager;
37use crate::{FromSync, SessionConfig, ToSync};
38
39static CHANNEL_BUFFER: usize = 1028;
40
41pub type ToTopicSync<E> = ToSync<Operation<E>>;
42
43/// Create and manage topic log sync sessions.
44///
45/// Sync sessions are created via the manager, which instantiates them with access to the shared
46/// topic map and operation store as well as channels for receiving sync events and for sending
47/// newly arriving operations in live mode.
48///
49/// A handle can be acquired to a sync session via the session_handle method for sending any live
50/// mode operations to a specific session. It's expected that users map sessions (by their id) to
51/// any topic subscriptions in order to understand the correct mappings.  
52///
53/// There are two points where message deduplication occurs; 1) per-subscription deduplication of
54/// received operations across all sessions for this manager occurs in the event stream 2)
55/// per-session deduplication occurs on sending operations to a remote, this is especially
56/// required during live-mode when we may receive the same new operation from several sources. The
57/// former stops any local consumers from receiving the same operation more than once, the latter
58/// reduces unnecessary noise on the network.
59#[allow(clippy::type_complexity)]
60#[derive(Debug)]
61pub struct TopicSyncManager<T, S, L, E>
62where
63    T: Clone,
64    E: Extensions,
65{
66    store: S,
67    session_topic_map: SessionTopicMap<T, mpsc::Sender<ToTopicSync<E>>>,
68    from_session_tx: HashMap<(u64, VerifyingKey), broadcast::Sender<TopicLogSyncEvent<E>>>,
69    from_session_rx: HashMap<(u64, VerifyingKey), broadcast::Receiver<TopicLogSyncEvent<E>>>,
70    manager_tx: Vec<mpsc::Sender<SessionStream<T, E>>>,
71    _phantom: PhantomData<L>,
72}
73
74#[derive(Debug)]
75pub(crate) struct SessionStream<T, E>
76where
77    T: Clone,
78    E: Clone,
79{
80    pub session_id: u64,
81    pub topic: T,
82    pub remote: VerifyingKey,
83    pub event_rx: broadcast::Receiver<TopicLogSyncEvent<E>>,
84    pub live_tx: mpsc::Sender<ToTopicSync<E>>,
85}
86
87impl<T, S, L, E> TopicSyncManager<T, S, L, E>
88where
89    T: Clone,
90    E: Extensions,
91{
92    pub fn new(store: S) -> Self {
93        Self {
94            store,
95            manager_tx: Default::default(),
96            session_topic_map: Default::default(),
97            from_session_tx: Default::default(),
98            from_session_rx: Default::default(),
99            _phantom: PhantomData,
100        }
101    }
102}
103
104impl<T, S, L, E> Manager<T> for TopicSyncManager<T, S, L, E>
105where
106    T: Clone + Debug + Eq + StdHash + Serialize + for<'a> Deserialize<'a> + Send + 'static,
107    S: LogStore<Operation<E>, VerifyingKey, L, u64, Hash>
108        + TopicStore<T, VerifyingKey, L>
109        + Clone
110        + Send
111        + 'static,
112    L: LogId + Debug + Send + 'static,
113    E: Extensions + Send + 'static,
114{
115    type Protocol = TopicLogSync<T, S, L, E>;
116    type Args = S;
117    type Event = TopicLogSyncEvent<E>;
118    type Message = Operation<E>;
119    type Error = TopicSyncManagerError;
120
121    /// Instantiate a manager from arguments.
122    fn from_args(store: Self::Args) -> Self {
123        Self::new(store)
124    }
125
126    /// Instantiate a new sync session.
127    async fn session(&mut self, session_id: u64, config: &SessionConfig<T>) -> Self::Protocol {
128        let (live_tx, live_rx) = mpsc::channel(CHANNEL_BUFFER);
129        let (event_tx, event_rx) = broadcast::channel::<Self::Event>(CHANNEL_BUFFER);
130
131        self.from_session_tx
132            .insert((session_id, config.remote), event_tx.clone());
133
134        self.from_session_rx
135            .insert((session_id, config.remote), event_rx);
136
137        self.session_topic_map
138            .insert_with_topic(session_id, config.topic.clone(), live_tx.clone());
139
140        for manager_tx in self.manager_tx.iter_mut() {
141            if let Err(err) = manager_tx
142                .send(SessionStream {
143                    session_id,
144                    topic: config.topic.clone(),
145                    remote: config.remote,
146                    event_rx: event_tx.subscribe(),
147                    live_tx: live_tx.clone(),
148                })
149                .await
150            {
151                debug!("manager handle dropped: {err:?}");
152            };
153        }
154
155        let live_rx = if config.live_mode {
156            Some(live_rx)
157        } else {
158            None
159        };
160
161        TopicLogSync::new(config.topic.clone(), self.store.clone(), live_rx, event_tx)
162    }
163
164    /// Retrieve a handle to a running sync session.
165    async fn session_handle(
166        &self,
167        session_id: u64,
168    ) -> Option<Pin<Box<dyn Sink<ToTopicSync<E>, Error = Self::Error>>>> {
169        let map_fn = |to_sync: ToSync<Self::Message>| {
170            ready({
171                match to_sync {
172                    ToSync::Payload(operation) => Ok::<_, Self::Error>(ToSync::Payload(operation)),
173                    ToSync::Close => Ok::<_, Self::Error>(ToSync::Close),
174                }
175            })
176        };
177
178        self.session_topic_map.sender(session_id).map(|tx| {
179            Box::pin(tx.clone().with(map_fn))
180                as Pin<Box<dyn Sink<ToTopicSync<E>, Error = Self::Error>>>
181        })
182    }
183
184    /// Subscribe to the event stream for all running sync sessions.
185    fn subscribe(&mut self) -> impl Stream<Item = FromSync<Self::Event>> + Send + Unpin + 'static {
186        let (manager_tx, manager_rx) = mpsc::channel(CHANNEL_BUFFER);
187        self.manager_tx.push(manager_tx);
188
189        let mut session_rx_set = SelectAll::new();
190        for ((id, remote), tx) in self.from_session_tx.iter() {
191            let session_id = *id;
192            let remote = *remote;
193            let stream = BroadcastStream::new(tx.subscribe());
194
195            #[allow(clippy::type_complexity)]
196            let stream: Pin<
197                Box<dyn StreamDebug<Option<FromSync<TopicLogSyncEvent<E>>>>>,
198            > = Box::pin(stream.map(Box::new(
199                move |event: Result<TopicLogSyncEvent<E>, BroadcastStreamRecvError>| {
200                    event.ok().map(|event| FromSync {
201                        session_id,
202                        remote,
203                        event,
204                    })
205                },
206            )));
207            session_rx_set.push(stream);
208        }
209
210        let state = ManagerEventStreamState {
211            manager_rx,
212            session_rx_set,
213            session_topic_map: self.session_topic_map.clone(),
214            dedup: DeduplicationBuffer::default(),
215        };
216
217        let stream = ManagerEventStream {
218            state: Some(state),
219            pending: Default::default(),
220        };
221
222        Box::pin(stream)
223    }
224}
225
226/// Error types which can be returned from `TopicSyncManager`.
227#[derive(Debug, Error)]
228pub enum TopicSyncManagerError {
229    #[error(transparent)]
230    TopicLogSync(#[from] TopicLogSyncError),
231
232    #[error("received operation before topic agreed")]
233    OperationBeforeTopic,
234
235    #[error(transparent)]
236    Send(#[from] mpsc::SendError),
237}
238#[cfg(test)]
239mod tests {
240    use std::collections::BTreeMap;
241    use std::time::Duration;
242
243    use assert_matches::assert_matches;
244    use futures::channel::mpsc;
245    use futures::{SinkExt, StreamExt};
246    use p2panda_core::test_utils::setup_logging;
247    use p2panda_core::{Body, Operation, Topic};
248    use p2panda_store::SqliteStore;
249
250    use crate::protocols::TopicLogSyncEvent;
251    use crate::test_utils::{Peer, TestTopicSyncManager, drain_stream, run_protocol};
252    use crate::traits::{Manager, Protocol};
253    use crate::{FromSync, SessionConfig, ToSync};
254
255    #[tokio::test]
256    async fn from_args() {
257        let store = SqliteStore::temporary().await;
258        let _: TestTopicSyncManager = Manager::from_args(store);
259    }
260
261    #[tokio::test]
262    async fn manager_e2e() {
263        setup_logging();
264
265        const LOG_ID: u64 = 0;
266        const SESSION_ID: u64 = 0;
267
268        let topic = Topic::random();
269
270        // Setup Peer A
271        let mut peer_a = Peer::new(0).await;
272        let body = Body::new("Hello from Peer A".as_bytes());
273        let _ = peer_a.create_operation(&body, LOG_ID).await;
274        let logs = BTreeMap::from([(peer_a.id(), vec![LOG_ID])]);
275        peer_a.associate(&topic, &logs).await;
276        let mut peer_a_manager = TestTopicSyncManager::new(peer_a.store.clone());
277
278        // Setup Peer B
279        let mut peer_b = Peer::new(1).await;
280        let body = Body::new("Hello from Peer B".as_bytes());
281        let _ = peer_b.create_operation(&body, LOG_ID).await;
282        let logs = BTreeMap::from([(peer_b.id(), vec![LOG_ID])]);
283        peer_b.associate(&topic, &logs).await;
284        let mut peer_b_manager = TestTopicSyncManager::new(peer_b.store.clone());
285
286        let config = SessionConfig {
287            topic,
288            remote: peer_b.id(),
289            live_mode: true,
290        };
291
292        // Subscribe to both managers.
293        let mut event_stream_a = peer_a_manager.subscribe();
294        let mut event_stream_b = peer_b_manager.subscribe();
295
296        // Instantiate sync sessions.
297        let peer_a_session = peer_a_manager.session(SESSION_ID, &config).await;
298        let peer_b_session = peer_b_manager.session(SESSION_ID, &config).await;
299
300        // Get a handle to Peer A sync session.
301        let mut peer_a_handle = peer_a_manager.session_handle(SESSION_ID).await.unwrap();
302
303        // Create and send a new live-mode message.
304        let (header_1, _) = peer_a.create_operation_no_insert(&body, LOG_ID).await;
305        peer_a_handle
306            .send(ToSync::Payload(Operation {
307                hash: header_1.hash(),
308                header: header_1.clone(),
309                body: Some(body.clone()),
310            }))
311            .await
312            .unwrap();
313        peer_a_handle.send(ToSync::Close).await.unwrap();
314
315        // Actually run the protocol.
316        run_protocol(peer_a_session, peer_b_session).await.unwrap();
317
318        // Assert Peer A's events.
319        for index in 0..=4 {
320            let event = event_stream_a.next().await.unwrap();
321            assert_eq!(event.session_id(), 0);
322            match index {
323                0 => assert_matches!(
324                    event,
325                    FromSync {
326                        event: TopicLogSyncEvent::SyncStarted { .. },
327                        ..
328                    }
329                ),
330                1 => assert_matches!(
331                    event,
332                    FromSync {
333                        event: TopicLogSyncEvent::OperationReceived { .. },
334                        ..
335                    }
336                ),
337                2 => assert_matches!(
338                    event,
339                    FromSync {
340                        event: TopicLogSyncEvent::SyncFinished { .. },
341                        ..
342                    }
343                ),
344                3 => assert_matches!(
345                    event,
346                    FromSync {
347                        event: TopicLogSyncEvent::LiveModeStarted,
348                        ..
349                    }
350                ),
351                4 => assert_matches!(
352                    event,
353                    FromSync {
354                        event: TopicLogSyncEvent::SessionFinished { .. },
355                        ..
356                    }
357                ),
358                _ => panic!(),
359            }
360        }
361
362        // Assert Peer B's events.
363        for index in 0..=5 {
364            let event = event_stream_b.next().await.unwrap();
365            match index {
366                0 => assert_matches!(
367                    event,
368                    FromSync {
369                        session_id: 0,
370                        event: TopicLogSyncEvent::SyncStarted { .. },
371                        ..
372                    }
373                ),
374                1 => assert_matches!(
375                    event,
376                    FromSync {
377                        session_id: 0,
378                        event: TopicLogSyncEvent::OperationReceived { .. },
379                        ..
380                    }
381                ),
382                2 => assert_matches!(
383                    event,
384                    FromSync {
385                        session_id: 0,
386                        event: TopicLogSyncEvent::SyncFinished { .. },
387                        ..
388                    }
389                ),
390                3 => assert_matches!(
391                    event,
392                    FromSync {
393                        event: TopicLogSyncEvent::LiveModeStarted,
394                        ..
395                    }
396                ),
397                4 => assert_matches!(
398                    event,
399                    FromSync {
400                        session_id: 0,
401                        event: TopicLogSyncEvent::OperationReceived { .. },
402                        ..
403                    }
404                ),
405                5 => assert_matches!(
406                    event,
407                    FromSync {
408                        event: TopicLogSyncEvent::SessionFinished { .. },
409                        ..
410                    }
411                ),
412                _ => panic!(),
413            }
414        }
415    }
416
417    #[tokio::test]
418    async fn live_mode_three_peer_forwarding() {
419        setup_logging();
420
421        const LOG_ID: u64 = 0;
422        const SESSION_AB: u64 = 0;
423        const SESSION_AC: u64 = 1;
424        const SESSION_BA: u64 = 2;
425        const SESSION_CA: u64 = 3;
426
427        let topic = Topic::random();
428
429        // Peer A
430        let mut peer_a = Peer::new(0).await;
431        let body_a = Body::new("Hello from A".as_bytes());
432        let (peer_a_header_0, _) = peer_a.create_operation(&body_a, LOG_ID).await;
433        let mut manager_a = TestTopicSyncManager::new(peer_a.store.clone());
434
435        // Peer B
436        let mut peer_b = Peer::new(1).await;
437        let body_b = Body::new("Hello from B".as_bytes());
438        let (peer_b_header_0, _) = peer_b.create_operation(&body_b, LOG_ID).await;
439        let mut manager_b = TestTopicSyncManager::new(peer_b.store.clone());
440
441        // Peer C
442        let mut peer_c = Peer::new(2).await;
443        let body_c = Body::new("Hello from C".as_bytes());
444        let (peer_c_header_0, _) = peer_c.create_operation(&body_c, LOG_ID).await;
445        let mut manager_c = TestTopicSyncManager::new(peer_c.store.clone());
446
447        let logs = BTreeMap::from([
448            (peer_a.id(), vec![LOG_ID]),
449            (peer_b.id(), vec![LOG_ID]),
450            (peer_c.id(), vec![LOG_ID]),
451        ]);
452        peer_a.associate(&topic, &logs).await;
453        peer_b.associate(&topic, &logs).await;
454        peer_c.associate(&topic, &logs).await;
455
456        // Session A -> B
457        let mut config = SessionConfig {
458            topic: topic.clone(),
459            remote: peer_b.id(),
460            live_mode: true,
461        };
462        let session_ab = manager_a.session(SESSION_AB, &config).await;
463        config.remote = peer_a.id();
464        let session_ba = manager_b.session(SESSION_BA, &config).await;
465
466        // Session A -> C
467        let mut config = SessionConfig {
468            topic: topic.clone(),
469            remote: peer_c.id(),
470            live_mode: true,
471        };
472        let session_ac = manager_a.session(SESSION_AC, &config).await;
473        config.remote = peer_a.id();
474        let session_ca = manager_c.session(SESSION_CA, &config).await;
475
476        let mut event_stream_a = manager_a.subscribe();
477        let mut event_stream_b = manager_b.subscribe();
478        let mut event_stream_c = manager_c.subscribe();
479
480        // Run both protocols concurrently.
481        {
482            let (mut local_message_tx, local_message_rx) = mpsc::channel(128);
483            let (mut remote_message_tx, remote_message_rx) = mpsc::channel(128);
484            let mut local_message_rx = local_message_rx.map(Ok::<_, ()>);
485            let mut remote_message_rx = remote_message_rx.map(Ok::<_, ()>);
486            tokio::spawn(async move {
487                session_ab
488                    .run(&mut local_message_tx, &mut remote_message_rx)
489                    .await
490                    .unwrap();
491            });
492            tokio::spawn(async move {
493                session_ba
494                    .run(&mut remote_message_tx, &mut local_message_rx)
495                    .await
496                    .unwrap();
497            });
498        }
499
500        {
501            let (mut local_message_tx, local_message_rx) = mpsc::channel(128);
502            let (mut remote_message_tx, remote_message_rx) = mpsc::channel(128);
503            let mut local_message_rx = local_message_rx.map(Ok::<_, ()>);
504            let mut remote_message_rx = remote_message_rx.map(Ok::<_, ()>);
505            tokio::spawn(async move {
506                session_ac
507                    .run(&mut local_message_tx, &mut remote_message_rx)
508                    .await
509                    .unwrap();
510            });
511            tokio::spawn(async move {
512                session_ca
513                    .run(&mut remote_message_tx, &mut local_message_rx)
514                    .await
515                    .unwrap();
516            });
517        }
518
519        // Send live-mode messages from all peers.
520        let mut handle_ab = manager_a.session_handle(SESSION_AB).await.unwrap();
521        let mut handle_ac = manager_a.session_handle(SESSION_AC).await.unwrap();
522        let mut handle_ba = manager_b.session_handle(SESSION_BA).await.unwrap();
523        let mut handle_ca = manager_c.session_handle(SESSION_CA).await.unwrap();
524
525        let body_a = Body::new("Hello again from A".as_bytes());
526        let body_b = Body::new("Hello again from B".as_bytes());
527        let body_c = Body::new("Hello again from C".as_bytes());
528        let (peer_a_header_1, _) = peer_a.create_operation(&body_a, LOG_ID).await;
529        let (peer_b_header_1, _) = peer_b.create_operation(&body_b, LOG_ID).await;
530        let (peer_c_header_1, _) = peer_c.create_operation(&body_c, LOG_ID).await;
531
532        let operation_a = Operation {
533            hash: peer_a_header_1.hash(),
534            header: peer_a_header_1.clone(),
535            body: Some(body_a.clone()),
536        };
537        let operation_b = Operation {
538            hash: peer_b_header_1.hash(),
539            header: peer_b_header_1.clone(),
540            body: Some(body_b.clone()),
541        };
542        let operation_c = Operation {
543            hash: peer_c_header_1.hash(),
544            header: peer_c_header_1.clone(),
545            body: Some(body_c.clone()),
546        };
547
548        handle_ab
549            .send(ToSync::Payload(operation_a.clone()))
550            .await
551            .unwrap();
552        handle_ac.send(ToSync::Payload(operation_a)).await.unwrap();
553        handle_ba.send(ToSync::Payload(operation_b)).await.unwrap();
554        handle_ca.send(ToSync::Payload(operation_c)).await.unwrap();
555
556        // Collect all operations each peer receives on the event stream.
557        let mut operations_a = vec![];
558        let mut operations_b = vec![];
559        let mut operations_c = vec![];
560        let _ = tokio::time::timeout(Duration::from_millis(500), async {
561            loop {
562                tokio::select! {
563                    Some(event) = event_stream_a.next() => {
564                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
565                            operations_a.push(operation.header().clone());
566                        }
567                    }
568                    Some(event) = event_stream_b.next() => {
569                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
570                            operations_b.push(operation.header().clone());
571                        }
572                    }
573                    Some(event) = event_stream_c.next() => {
574                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
575                            operations_c.push(operation.header().clone());
576                        }
577                    }
578                    else => tokio::time::sleep(Duration::from_millis(20)).await
579                }
580            }
581        })
582        .await;
583
584        // A receives 2 operations each from B & C
585        assert_eq!(operations_a.len(), 4);
586        // B receives 2 operations each from A & C (via A)
587        assert_eq!(operations_b.len(), 4);
588        // C receives 2 operations each from A & B (via A)
589        assert_eq!(operations_c.len(), 4);
590
591        // No peers received their own operations back again.
592        assert!(!operations_a.contains(&peer_a_header_0));
593        assert!(!operations_a.contains(&peer_a_header_1));
594        assert!(!operations_b.contains(&peer_b_header_0));
595        assert!(!operations_b.contains(&peer_b_header_1));
596        assert!(!operations_c.contains(&peer_c_header_0));
597        assert!(!operations_c.contains(&peer_c_header_1));
598    }
599
600    #[tokio::test]
601    async fn non_blocking_manager_stream() {
602        const LOG_ID: u64 = 0;
603        const SESSION_ID: u64 = 0;
604
605        let topic = Topic::random();
606
607        // Setup Peer A
608        let mut peer_a = Peer::new(0).await;
609        let body = Body::new("Hello from Peer A".as_bytes());
610        let _ = peer_a.create_operation(&body, LOG_ID).await;
611        let logs = BTreeMap::from([(peer_a.id(), vec![LOG_ID])]);
612        peer_a.associate(&topic, &logs).await;
613        let mut peer_a_manager = TestTopicSyncManager::new(peer_a.store.clone());
614
615        // Spawn a task polling peer a's manager stream.
616        let mut peer_a_stream = peer_a_manager.subscribe();
617        tokio::task::spawn(async move {
618            loop {
619                peer_a_stream.next().await;
620            }
621        });
622
623        // Setup Peer B
624        let mut peer_b = Peer::new(1).await;
625        let body = Body::new("Hello from Peer B".as_bytes());
626        let _ = peer_b.create_operation(&body, LOG_ID).await;
627        let logs = BTreeMap::from([(peer_b.id(), vec![LOG_ID])]);
628        peer_b.associate(&topic, &logs).await;
629        let mut peer_b_manager = TestTopicSyncManager::new(peer_b.store.clone());
630
631        let config = SessionConfig {
632            topic,
633            remote: peer_b.id(),
634            live_mode: true,
635        };
636
637        let peer_a_session = peer_a_manager.session(SESSION_ID, &config).await;
638
639        let event_stream = peer_b_manager.subscribe();
640        let peer_b_session = peer_b_manager.session(SESSION_ID, &config).await;
641
642        // Get a handle to Peer A sync session.
643        let mut peer_a_handle = peer_a_manager.session_handle(SESSION_ID).await.unwrap();
644
645        // Create and send a new live-mode message.
646        let (header_1, _) = peer_a.create_operation_no_insert(&body, LOG_ID).await;
647        peer_a_handle
648            .send(ToSync::Payload(Operation {
649                hash: header_1.hash(),
650                header: header_1.clone(),
651                body: Some(body.clone()),
652            }))
653            .await
654            .unwrap();
655        peer_a_handle.send(ToSync::Close).await.unwrap();
656
657        // Actually run the protocol.
658        run_protocol(peer_a_session, peer_b_session).await.unwrap();
659
660        // Assert Peer B's events.
661        let events = drain_stream(event_stream).await;
662        assert_eq!(events.len(), 6);
663        for (index, event) in events.into_iter().enumerate() {
664            match index {
665                0 => assert_matches!(
666                    event,
667                    FromSync {
668                        session_id: 0,
669                        event: TopicLogSyncEvent::SyncStarted { .. },
670                        ..
671                    }
672                ),
673                1 => assert_matches!(
674                    event,
675                    FromSync {
676                        session_id: 0,
677                        event: TopicLogSyncEvent::OperationReceived { .. },
678                        ..
679                    }
680                ),
681                2 => assert_matches!(
682                    event,
683                    FromSync {
684                        session_id: 0,
685                        event: TopicLogSyncEvent::SyncFinished { .. },
686                        ..
687                    }
688                ),
689                3 => assert_matches!(
690                    event,
691                    FromSync {
692                        event: TopicLogSyncEvent::LiveModeStarted,
693                        ..
694                    }
695                ),
696                4 => assert_matches!(
697                    event,
698                    FromSync {
699                        session_id: 0,
700                        event: TopicLogSyncEvent::OperationReceived { .. },
701                        ..
702                    }
703                ),
704                5 => assert_matches!(
705                    event,
706                    FromSync {
707                        event: TopicLogSyncEvent::SessionFinished { .. },
708                        ..
709                    }
710                ),
711                _ => panic!(),
712            }
713        }
714    }
715}