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