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::{Body, Operation, Topic};
247    use p2panda_store::SqliteStore;
248
249    use crate::protocols::TopicLogSyncEvent;
250    use crate::test_utils::{
251        Peer, TestTopicSyncManager, drain_stream, run_protocol, setup_logging,
252    };
253    use crate::traits::{Manager, Protocol};
254    use crate::{FromSync, SessionConfig, ToSync};
255
256    #[tokio::test]
257    async fn from_args() {
258        let store = SqliteStore::temporary().await;
259        let _: TestTopicSyncManager = Manager::from_args(store);
260    }
261
262    #[tokio::test]
263    async fn manager_e2e() {
264        setup_logging();
265
266        const LOG_ID: u64 = 0;
267        const SESSION_ID: u64 = 0;
268
269        let topic = Topic::random();
270
271        // Setup Peer A
272        let mut peer_a = Peer::new(0).await;
273        let body = Body::new("Hello from Peer A".as_bytes());
274        let _ = peer_a.create_operation(&body, LOG_ID).await;
275        let logs = BTreeMap::from([(peer_a.id(), vec![LOG_ID])]);
276        peer_a.associate(&topic, &logs).await;
277        let mut peer_a_manager = TestTopicSyncManager::new(peer_a.store.clone());
278
279        // Setup Peer B
280        let mut peer_b = Peer::new(1).await;
281        let body = Body::new("Hello from Peer B".as_bytes());
282        let _ = peer_b.create_operation(&body, LOG_ID).await;
283        let logs = BTreeMap::from([(peer_b.id(), vec![LOG_ID])]);
284        peer_b.associate(&topic, &logs).await;
285        let mut peer_b_manager = TestTopicSyncManager::new(peer_b.store.clone());
286
287        let config = SessionConfig {
288            topic,
289            remote: peer_b.id(),
290            live_mode: true,
291        };
292
293        // Subscribe to both managers.
294        let mut event_stream_a = peer_a_manager.subscribe();
295        let mut event_stream_b = peer_b_manager.subscribe();
296
297        // Instantiate sync sessions.
298        let peer_a_session = peer_a_manager.session(SESSION_ID, &config).await;
299        let peer_b_session = peer_b_manager.session(SESSION_ID, &config).await;
300
301        // Get a handle to Peer A sync session.
302        let mut peer_a_handle = peer_a_manager.session_handle(SESSION_ID).await.unwrap();
303
304        // Create and send a new live-mode message.
305        let (header_1, _) = peer_a.create_operation_no_insert(&body, LOG_ID).await;
306        peer_a_handle
307            .send(ToSync::Payload(Operation {
308                hash: header_1.hash(),
309                header: header_1.clone(),
310                body: Some(body.clone()),
311            }))
312            .await
313            .unwrap();
314        peer_a_handle.send(ToSync::Close).await.unwrap();
315
316        // Actually run the protocol.
317        run_protocol(peer_a_session, peer_b_session).await.unwrap();
318
319        // Assert Peer A's events.
320        for index in 0..=4 {
321            let event = event_stream_a.next().await.unwrap();
322            assert_eq!(event.session_id(), 0);
323            match index {
324                0 => assert_matches!(
325                    event,
326                    FromSync {
327                        event: TopicLogSyncEvent::SyncStarted { .. },
328                        ..
329                    }
330                ),
331                1 => assert_matches!(
332                    event,
333                    FromSync {
334                        event: TopicLogSyncEvent::OperationReceived { .. },
335                        ..
336                    }
337                ),
338                2 => assert_matches!(
339                    event,
340                    FromSync {
341                        event: TopicLogSyncEvent::SyncFinished { .. },
342                        ..
343                    }
344                ),
345                3 => assert_matches!(
346                    event,
347                    FromSync {
348                        event: TopicLogSyncEvent::LiveModeStarted,
349                        ..
350                    }
351                ),
352                4 => assert_matches!(
353                    event,
354                    FromSync {
355                        event: TopicLogSyncEvent::SessionFinished { .. },
356                        ..
357                    }
358                ),
359                _ => panic!(),
360            }
361        }
362
363        // Assert Peer B's events.
364        for index in 0..=5 {
365            let event = event_stream_b.next().await.unwrap();
366            match index {
367                0 => assert_matches!(
368                    event,
369                    FromSync {
370                        session_id: 0,
371                        event: TopicLogSyncEvent::SyncStarted { .. },
372                        ..
373                    }
374                ),
375                1 => assert_matches!(
376                    event,
377                    FromSync {
378                        session_id: 0,
379                        event: TopicLogSyncEvent::OperationReceived { .. },
380                        ..
381                    }
382                ),
383                2 => assert_matches!(
384                    event,
385                    FromSync {
386                        session_id: 0,
387                        event: TopicLogSyncEvent::SyncFinished { .. },
388                        ..
389                    }
390                ),
391                3 => assert_matches!(
392                    event,
393                    FromSync {
394                        event: TopicLogSyncEvent::LiveModeStarted,
395                        ..
396                    }
397                ),
398                4 => assert_matches!(
399                    event,
400                    FromSync {
401                        session_id: 0,
402                        event: TopicLogSyncEvent::OperationReceived { .. },
403                        ..
404                    }
405                ),
406                5 => assert_matches!(
407                    event,
408                    FromSync {
409                        event: TopicLogSyncEvent::SessionFinished { .. },
410                        ..
411                    }
412                ),
413                _ => panic!(),
414            }
415        }
416    }
417
418    #[tokio::test]
419    async fn live_mode_three_peer_forwarding() {
420        setup_logging();
421
422        const LOG_ID: u64 = 0;
423        const SESSION_AB: u64 = 0;
424        const SESSION_AC: u64 = 1;
425        const SESSION_BA: u64 = 2;
426        const SESSION_CA: u64 = 3;
427
428        let topic = Topic::random();
429
430        // Peer A
431        let mut peer_a = Peer::new(0).await;
432        let body_a = Body::new("Hello from A".as_bytes());
433        let (peer_a_header_0, _) = peer_a.create_operation(&body_a, LOG_ID).await;
434        let mut manager_a = TestTopicSyncManager::new(peer_a.store.clone());
435
436        // Peer B
437        let mut peer_b = Peer::new(1).await;
438        let body_b = Body::new("Hello from B".as_bytes());
439        let (peer_b_header_0, _) = peer_b.create_operation(&body_b, LOG_ID).await;
440        let mut manager_b = TestTopicSyncManager::new(peer_b.store.clone());
441
442        // Peer C
443        let mut peer_c = Peer::new(2).await;
444        let body_c = Body::new("Hello from C".as_bytes());
445        let (peer_c_header_0, _) = peer_c.create_operation(&body_c, LOG_ID).await;
446        let mut manager_c = TestTopicSyncManager::new(peer_c.store.clone());
447
448        let logs = BTreeMap::from([
449            (peer_a.id(), vec![LOG_ID]),
450            (peer_b.id(), vec![LOG_ID]),
451            (peer_c.id(), vec![LOG_ID]),
452        ]);
453        peer_a.associate(&topic, &logs).await;
454        peer_b.associate(&topic, &logs).await;
455        peer_c.associate(&topic, &logs).await;
456
457        // Session A -> B
458        let mut config = SessionConfig {
459            topic: topic.clone(),
460            remote: peer_b.id(),
461            live_mode: true,
462        };
463        let session_ab = manager_a.session(SESSION_AB, &config).await;
464        config.remote = peer_a.id();
465        let session_ba = manager_b.session(SESSION_BA, &config).await;
466
467        // Session A -> C
468        let mut config = SessionConfig {
469            topic: topic.clone(),
470            remote: peer_c.id(),
471            live_mode: true,
472        };
473        let session_ac = manager_a.session(SESSION_AC, &config).await;
474        config.remote = peer_a.id();
475        let session_ca = manager_c.session(SESSION_CA, &config).await;
476
477        let mut event_stream_a = manager_a.subscribe();
478        let mut event_stream_b = manager_b.subscribe();
479        let mut event_stream_c = manager_c.subscribe();
480
481        // Run both protocols concurrently.
482        {
483            let (mut local_message_tx, local_message_rx) = mpsc::channel(128);
484            let (mut remote_message_tx, remote_message_rx) = mpsc::channel(128);
485            let mut local_message_rx = local_message_rx.map(Ok::<_, ()>);
486            let mut remote_message_rx = remote_message_rx.map(Ok::<_, ()>);
487            tokio::spawn(async move {
488                session_ab
489                    .run(&mut local_message_tx, &mut remote_message_rx)
490                    .await
491                    .unwrap();
492            });
493            tokio::spawn(async move {
494                session_ba
495                    .run(&mut remote_message_tx, &mut local_message_rx)
496                    .await
497                    .unwrap();
498            });
499        }
500
501        {
502            let (mut local_message_tx, local_message_rx) = mpsc::channel(128);
503            let (mut remote_message_tx, remote_message_rx) = mpsc::channel(128);
504            let mut local_message_rx = local_message_rx.map(Ok::<_, ()>);
505            let mut remote_message_rx = remote_message_rx.map(Ok::<_, ()>);
506            tokio::spawn(async move {
507                session_ac
508                    .run(&mut local_message_tx, &mut remote_message_rx)
509                    .await
510                    .unwrap();
511            });
512            tokio::spawn(async move {
513                session_ca
514                    .run(&mut remote_message_tx, &mut local_message_rx)
515                    .await
516                    .unwrap();
517            });
518        }
519
520        // Send live-mode messages from all peers.
521        let mut handle_ab = manager_a.session_handle(SESSION_AB).await.unwrap();
522        let mut handle_ac = manager_a.session_handle(SESSION_AC).await.unwrap();
523        let mut handle_ba = manager_b.session_handle(SESSION_BA).await.unwrap();
524        let mut handle_ca = manager_c.session_handle(SESSION_CA).await.unwrap();
525
526        let body_a = Body::new("Hello again from A".as_bytes());
527        let body_b = Body::new("Hello again from B".as_bytes());
528        let body_c = Body::new("Hello again from C".as_bytes());
529        let (peer_a_header_1, _) = peer_a.create_operation(&body_a, LOG_ID).await;
530        let (peer_b_header_1, _) = peer_b.create_operation(&body_b, LOG_ID).await;
531        let (peer_c_header_1, _) = peer_c.create_operation(&body_c, LOG_ID).await;
532
533        let operation_a = Operation {
534            hash: peer_a_header_1.hash(),
535            header: peer_a_header_1.clone(),
536            body: Some(body_a.clone()),
537        };
538        let operation_b = Operation {
539            hash: peer_b_header_1.hash(),
540            header: peer_b_header_1.clone(),
541            body: Some(body_b.clone()),
542        };
543        let operation_c = Operation {
544            hash: peer_c_header_1.hash(),
545            header: peer_c_header_1.clone(),
546            body: Some(body_c.clone()),
547        };
548
549        handle_ab
550            .send(ToSync::Payload(operation_a.clone()))
551            .await
552            .unwrap();
553        handle_ac.send(ToSync::Payload(operation_a)).await.unwrap();
554        handle_ba.send(ToSync::Payload(operation_b)).await.unwrap();
555        handle_ca.send(ToSync::Payload(operation_c)).await.unwrap();
556
557        // Collect all operations each peer receives on the event stream.
558        let mut operations_a = vec![];
559        let mut operations_b = vec![];
560        let mut operations_c = vec![];
561        let _ = tokio::time::timeout(Duration::from_millis(500), async {
562            loop {
563                tokio::select! {
564                    Some(event) = event_stream_a.next() => {
565                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
566                            println!("A received operation: {}", operation.hash);
567                            operations_a.push(operation.header().clone());
568                        }
569                    }
570                    Some(event) = event_stream_b.next() => {
571                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
572                            println!("B received operation: {}", operation.hash);
573                            operations_b.push(operation.header().clone());
574                        }
575                    }
576                    Some(event) = event_stream_c.next() => {
577                        if let TopicLogSyncEvent::OperationReceived { operation, .. } = event.event() {
578                            operations_c.push(operation.header().clone());
579                        }
580                    }
581                    else => tokio::time::sleep(Duration::from_millis(20)).await
582                }
583            }
584        })
585        .await;
586
587        // A receives 2 operations each from B & C
588        assert_eq!(operations_a.len(), 4);
589        // B receives 2 operations each from A & C (via A)
590        assert_eq!(operations_b.len(), 4);
591        // C receives 2 operations each from A & B (via A)
592        assert_eq!(operations_c.len(), 4);
593
594        // No peers received their own operations back again.
595        assert!(!operations_a.contains(&peer_a_header_0));
596        assert!(!operations_a.contains(&peer_a_header_1));
597        assert!(!operations_b.contains(&peer_b_header_0));
598        assert!(!operations_b.contains(&peer_b_header_1));
599        assert!(!operations_c.contains(&peer_c_header_0));
600        assert!(!operations_c.contains(&peer_c_header_1));
601    }
602
603    #[tokio::test]
604    async fn non_blocking_manager_stream() {
605        const LOG_ID: u64 = 0;
606        const SESSION_ID: u64 = 0;
607
608        let topic = Topic::random();
609
610        // Setup Peer A
611        let mut peer_a = Peer::new(0).await;
612        let body = Body::new("Hello from Peer A".as_bytes());
613        let _ = peer_a.create_operation(&body, LOG_ID).await;
614        let logs = BTreeMap::from([(peer_a.id(), vec![LOG_ID])]);
615        peer_a.associate(&topic, &logs).await;
616        let mut peer_a_manager = TestTopicSyncManager::new(peer_a.store.clone());
617
618        // Spawn a task polling peer a's manager stream.
619        let mut peer_a_stream = peer_a_manager.subscribe();
620        tokio::task::spawn(async move {
621            loop {
622                peer_a_stream.next().await;
623            }
624        });
625
626        // Setup Peer B
627        let mut peer_b = Peer::new(1).await;
628        let body = Body::new("Hello from Peer B".as_bytes());
629        let _ = peer_b.create_operation(&body, LOG_ID).await;
630        let logs = BTreeMap::from([(peer_b.id(), vec![LOG_ID])]);
631        peer_b.associate(&topic, &logs).await;
632        let mut peer_b_manager = TestTopicSyncManager::new(peer_b.store.clone());
633
634        let config = SessionConfig {
635            topic,
636            remote: peer_b.id(),
637            live_mode: true,
638        };
639
640        let peer_a_session = peer_a_manager.session(SESSION_ID, &config).await;
641
642        let event_stream = peer_b_manager.subscribe();
643        let peer_b_session = peer_b_manager.session(SESSION_ID, &config).await;
644
645        // Get a handle to Peer A sync session.
646        let mut peer_a_handle = peer_a_manager.session_handle(SESSION_ID).await.unwrap();
647
648        // Create and send a new live-mode message.
649        let (header_1, _) = peer_a.create_operation_no_insert(&body, LOG_ID).await;
650        peer_a_handle
651            .send(ToSync::Payload(Operation {
652                hash: header_1.hash(),
653                header: header_1.clone(),
654                body: Some(body.clone()),
655            }))
656            .await
657            .unwrap();
658        peer_a_handle.send(ToSync::Close).await.unwrap();
659
660        // Actually run the protocol.
661        run_protocol(peer_a_session, peer_b_session).await.unwrap();
662
663        // Assert Peer B's events.
664        let events = drain_stream(event_stream).await;
665        assert_eq!(events.len(), 6);
666        for (index, event) in events.into_iter().enumerate() {
667            match index {
668                0 => assert_matches!(
669                    event,
670                    FromSync {
671                        session_id: 0,
672                        event: TopicLogSyncEvent::SyncStarted { .. },
673                        ..
674                    }
675                ),
676                1 => assert_matches!(
677                    event,
678                    FromSync {
679                        session_id: 0,
680                        event: TopicLogSyncEvent::OperationReceived { .. },
681                        ..
682                    }
683                ),
684                2 => assert_matches!(
685                    event,
686                    FromSync {
687                        session_id: 0,
688                        event: TopicLogSyncEvent::SyncFinished { .. },
689                        ..
690                    }
691                ),
692                3 => assert_matches!(
693                    event,
694                    FromSync {
695                        event: TopicLogSyncEvent::LiveModeStarted,
696                        ..
697                    }
698                ),
699                4 => assert_matches!(
700                    event,
701                    FromSync {
702                        session_id: 0,
703                        event: TopicLogSyncEvent::OperationReceived { .. },
704                        ..
705                    }
706                ),
707                5 => assert_matches!(
708                    event,
709                    FromSync {
710                        event: TopicLogSyncEvent::SessionFinished { .. },
711                        ..
712                    }
713                ),
714                _ => panic!(),
715            }
716        }
717    }
718}