1use std::collections::HashMap;
10use std::fmt;
11use std::net::AddrParseError;
12use std::path::Path;
13use std::sync::Mutex;
14
15use libsql::{named_params, params};
16use tokio::sync::Mutex as AsyncMutex;
17
18use crate::types::{
19 ChannelKind, ChannelState, DcOption, PeerAuth, PeerId, PeerInfo, PeerKind, UpdateState,
20 UpdatesState,
21};
22use crate::{BoxFuture, DEFAULT_DC, KNOWN_DC_OPTIONS, Session};
23
24const VERSION: i64 = 1;
25
26struct Database(libsql::Connection);
27
28struct Cache {
29 pub home_dc: i32,
30 pub dc_options: HashMap<i32, DcOption>,
31}
32
33pub struct SqliteSession {
35 database: AsyncMutex<Database>,
36 cache: Mutex<Cache>,
37}
38
39#[derive(Debug)]
40pub enum SqliteSessionError {
41 Poisoned,
42 AddrParse(std::net::AddrParseError),
43 Sql(libsql::Error),
44 InvalidAuthKeyLength(usize),
45}
46
47impl std::error::Error for SqliteSessionError {}
48
49impl fmt::Display for SqliteSessionError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 SqliteSessionError::Poisoned => write!(f, "session lock is poisoned"),
53 SqliteSessionError::AddrParse(_) => write!(f, "invalid socket address syntax"),
54 SqliteSessionError::Sql(err) => write!(f, "{err}"),
55 SqliteSessionError::InvalidAuthKeyLength(actual) => {
56 write!(f, "invalid auth_key length: expected 256, got {actual}")
57 }
58 }
59 }
60}
61
62impl From<AddrParseError> for SqliteSessionError {
63 fn from(x: AddrParseError) -> Self {
64 Self::AddrParse(x)
65 }
66}
67
68impl From<libsql::Error> for SqliteSessionError {
69 fn from(x: libsql::Error) -> Self {
70 Self::Sql(x)
71 }
72}
73
74#[repr(u8)]
75enum PeerSubtype {
76 UserSelf = 1,
77 UserBot = 2,
78 UserSelfBot = 3,
79 Megagroup = 4,
80 Broadcast = 8,
81 Gigagroup = 12,
82 Community = 16,
83}
84
85impl Database {
86 async fn init(&self) -> libsql::Result<()> {
87 let mut user_version: i64 = self
88 .fetch_one("PRAGMA user_version", params![], |row| row.get(0))
89 .await?
90 .unwrap_or(0);
91 if user_version == VERSION {
92 return Ok(());
93 }
94
95 if user_version == 0 {
96 self.migrate_v0_to_v1().await?;
97 user_version += 1;
98 }
99 if user_version == VERSION {
100 self.0
102 .execute(&format!("PRAGMA user_version = {VERSION}"), params![])
103 .await?;
104 }
105 Ok(())
106 }
107
108 async fn migrate_v0_to_v1(&self) -> libsql::Result<()> {
109 let transaction = self.begin_transaction().await?;
110 transaction
111 .execute(
112 "CREATE TABLE dc_home (
113 dc_id INTEGER NOT NULL,
114 PRIMARY KEY(dc_id))",
115 params![],
116 )
117 .await?;
118 transaction
119 .execute(
120 "CREATE TABLE dc_option (
121 dc_id INTEGER NOT NULL,
122 ipv4 TEXT NOT NULL,
123 ipv6 TEXT NOT NULL,
124 auth_key BLOB,
125 PRIMARY KEY (dc_id))",
126 params![],
127 )
128 .await?;
129 transaction
130 .execute(
131 "CREATE TABLE peer_info (
132 peer_id INTEGER NOT NULL,
133 hash INTEGER,
134 subtype INTEGER,
135 PRIMARY KEY (peer_id))",
136 params![],
137 )
138 .await?;
139 transaction
140 .execute(
141 "CREATE TABLE update_state (
142 pts INTEGER NOT NULL,
143 qts INTEGER NOT NULL,
144 date INTEGER NOT NULL,
145 seq INTEGER NOT NULL)",
146 params![],
147 )
148 .await?;
149 transaction
150 .execute(
151 "CREATE TABLE channel_state (
152 peer_id INTEGER NOT NULL,
153 pts INTEGER NOT NULL,
154 PRIMARY KEY (peer_id))",
155 params![],
156 )
157 .await?;
158
159 transaction.commit().await?;
160 Ok(())
161 }
162
163 async fn begin_transaction(&self) -> libsql::Result<libsql::Transaction> {
164 self.0.transaction().await
165 }
166
167 async fn fetch_one<
168 T,
169 P: libsql::params::IntoParams,
170 F: FnOnce(libsql::Row) -> libsql::Result<T>,
171 >(
172 &self,
173 statement: &str,
174 params: P,
175 select: F,
176 ) -> libsql::Result<Option<T>> {
177 let mut statement = self.0.prepare(statement).await?;
178 let result = statement.query_row(params).await;
179 match result {
180 Ok(value) => Ok(Some(select(value)?)),
181 Err(libsql::Error::QueryReturnedNoRows) => Ok(None),
182 Err(e) => Err(e),
183 }
184 }
185
186 async fn fetch_all<
187 T,
188 P: libsql::params::IntoParams,
189 F: FnMut(libsql::Row) -> Result<T, SqliteSessionError>,
190 >(
191 &self,
192 statement: &str,
193 params: P,
194 mut select: F,
195 ) -> Result<Vec<T>, SqliteSessionError> {
196 let statement = self.0.prepare(statement).await?;
197 let mut rows = statement.query(params).await?;
198 let mut result = Vec::new();
199 while let Some(row) = rows.next().await? {
200 result.push(select(row)?);
201 }
202 Ok(result)
203 }
204}
205
206impl SqliteSession {
207 pub async fn open<P: AsRef<Path>>(path: P) -> Result<Self, SqliteSessionError> {
210 let conn = libsql::Builder::new_local(path).build().await?.connect()?;
211 let db = Database(conn);
212 db.init().await?;
213
214 let home_dc = db
215 .fetch_one("SELECT * FROM dc_home LIMIT 1", named_params![], |row| {
216 row.get::<i32>(0)
217 })
218 .await?
219 .unwrap_or(DEFAULT_DC);
220
221 let dc_options = db
222 .fetch_all("SELECT * FROM dc_option", named_params![], |row| {
223 Ok(DcOption {
224 id: row.get::<i32>(0)?,
225 ipv4: row.get::<String>(1)?.parse()?,
226 ipv6: row.get::<String>(2)?.parse()?,
227 auth_key: match row.get::<Option<Vec<u8>>>(3)? {
228 None => None,
229 Some(auth_key) => Some(auth_key.try_into().map_err(|v: Vec<u8>| {
230 SqliteSessionError::InvalidAuthKeyLength(v.len())
231 })?),
232 },
233 })
234 })
235 .await?
236 .into_iter()
237 .map(|dc_option| (dc_option.id, dc_option))
238 .collect();
239
240 Ok(SqliteSession {
241 database: AsyncMutex::new(db),
242 cache: Mutex::new(Cache {
243 home_dc,
244 dc_options,
245 }),
246 })
247 }
248}
249
250impl Session for SqliteSession {
251 type Error = SqliteSessionError;
252
253 fn home_dc_id(&self) -> Result<i32, SqliteSessionError> {
254 Ok(self
255 .cache
256 .lock()
257 .map_err(|_| SqliteSessionError::Poisoned)?
258 .home_dc)
259 }
260
261 fn set_home_dc_id(&self, dc_id: i32) -> BoxFuture<'_, Result<(), SqliteSessionError>> {
262 let ok = match self.cache.lock() {
263 Err(_) => Err(SqliteSessionError::Poisoned),
264 Ok(mut x) => {
265 x.home_dc = dc_id;
266 Ok(())
267 }
268 };
269 Box::pin(async move {
270 ok?;
271
272 let transaction = self.database.lock().await.begin_transaction().await?;
273 transaction
274 .execute("DELETE FROM dc_home", params![])
275 .await?;
276 let stmt = transaction
277 .prepare("INSERT INTO dc_home VALUES (:dc_id)")
278 .await?;
279 stmt.execute(named_params! {":dc_id": dc_id}).await?;
280 transaction.commit().await?;
281 Ok(())
282 })
283 }
284
285 fn dc_option(&self, dc_id: i32) -> Result<Option<DcOption>, SqliteSessionError> {
286 Ok(self
287 .cache
288 .lock()
289 .map_err(|_| SqliteSessionError::Poisoned)?
290 .dc_options
291 .get(&dc_id)
292 .cloned()
293 .or_else(|| {
294 KNOWN_DC_OPTIONS
295 .iter()
296 .find(|dc_option| dc_option.id == dc_id)
297 .cloned()
298 }))
299 }
300
301 fn set_dc_option(&self, dc_option: &DcOption) -> BoxFuture<'_, Result<(), SqliteSessionError>> {
302 let ok = match self.cache.lock() {
303 Err(_) => Err(SqliteSessionError::Poisoned),
304 Ok(mut x) => {
305 x.dc_options.insert(dc_option.id, dc_option.clone());
306 Ok(())
307 }
308 };
309
310 let dc_option = dc_option.clone();
311 Box::pin(async move {
312 ok?;
313
314 let db = self.database.lock().await;
315 db.0.execute(
316 "INSERT OR REPLACE INTO dc_option VALUES (:dc_id, :ipv4, :ipv6, :auth_key)",
317 named_params! {
318 ":dc_id": dc_option.id,
319 ":ipv4": dc_option.ipv4.to_string(),
320 ":ipv6": dc_option.ipv6.to_string(),
321 ":auth_key": dc_option.auth_key.map(|k| k.to_vec()),
322 },
323 )
324 .await?;
325 Ok(())
326 })
327 }
328
329 fn peer(&self, peer: PeerId) -> BoxFuture<'_, Result<Option<PeerInfo>, SqliteSessionError>> {
330 Box::pin(async move {
331 let db = self.database.lock().await;
332 let map_row = |row: libsql::Row| {
333 let subtype = row.get::<Option<i64>>(2)?.map(|s| s as u8);
334 Ok(match peer.kind() {
335 PeerKind::User => PeerInfo::User {
336 id: PeerId::user_unchecked(row.get::<i64>(0)?).bare_id_unchecked(),
337 auth: row.get::<Option<i64>>(1)?.map(PeerAuth::from_hash),
338 bot: subtype.map(|s| s & PeerSubtype::UserBot as u8 != 0),
339 is_self: subtype.map(|s| s & PeerSubtype::UserSelf as u8 != 0),
340 },
341 PeerKind::Chat => PeerInfo::Chat {
342 id: peer.bare_id_unchecked(),
343 },
344 PeerKind::Channel => PeerInfo::Channel {
345 id: peer.bare_id_unchecked(),
346 auth: row.get::<Option<i64>>(1)?.map(PeerAuth::from_hash),
347 kind: subtype.and_then(|s| {
348 if (s & PeerSubtype::Gigagroup as u8) == PeerSubtype::Gigagroup as u8 {
349 Some(ChannelKind::Gigagroup)
350 } else if s & PeerSubtype::Broadcast as u8 != 0 {
351 Some(ChannelKind::Broadcast)
352 } else if s & PeerSubtype::Megagroup as u8 != 0 {
353 Some(ChannelKind::Megagroup)
354 } else {
355 None
356 }
357 }),
358 },
359 })
360 };
361
362 Ok(if let Some(peer_id) = peer.bot_api_dialog_id() {
363 db.fetch_one(
364 "SELECT * FROM peer_info WHERE peer_id = :peer_id LIMIT 1",
365 named_params! {":peer_id": peer_id},
366 map_row,
367 )
368 .await?
369 } else {
370 db.fetch_one(
371 "SELECT * FROM peer_info WHERE subtype & :type LIMIT 1",
372 named_params! {":type": PeerSubtype::UserSelf as i64},
373 map_row,
374 )
375 .await?
376 })
377 })
378 }
379
380 fn cache_peer(&self, peer: PeerInfo) -> BoxFuture<'_, Result<(), SqliteSessionError>> {
381 Box::pin(async move {
382 let peer = if let Some(mut existing_peer) = self.peer(peer.id()).await? {
383 existing_peer.extend_info(&peer);
384 existing_peer
385 } else {
386 peer
387 };
388
389 let db = self.database.lock().await;
390 let stmt =
391 db.0.prepare("INSERT OR REPLACE INTO peer_info VALUES (:peer_id, :hash, :subtype)")
392 .await?;
393 let subtype = match peer {
394 PeerInfo::User { bot, is_self, .. } => {
395 match (bot.unwrap_or_default(), is_self.unwrap_or_default()) {
396 (true, true) => Some(PeerSubtype::UserSelfBot),
397 (true, false) => Some(PeerSubtype::UserBot),
398 (false, true) => Some(PeerSubtype::UserSelf),
399 (false, false) => None,
400 }
401 }
402 PeerInfo::Chat { .. } => None,
403 PeerInfo::Channel { kind, .. } => kind.map(|kind| match kind {
404 ChannelKind::Megagroup => PeerSubtype::Megagroup,
405 ChannelKind::Broadcast => PeerSubtype::Broadcast,
406 ChannelKind::Gigagroup => PeerSubtype::Gigagroup,
407 ChannelKind::Community => PeerSubtype::Community,
408 }),
409 };
410 let mut params = vec![];
411 let peer_id = peer.id().bot_api_dialog_id_unchecked();
412 params.push((":peer_id".to_owned(), peer_id));
413 let hash = peer.auth().unwrap_or_default().hash();
414 if peer.auth().is_some() {
415 params.push((":hash".to_owned(), hash));
416 }
417 if let Some(subtype) = subtype.map(|s| s as i64) {
418 params.push((":subtype".to_owned(), subtype));
419 }
420 stmt.execute(params).await?;
421 Ok(())
422 })
423 }
424
425 fn cache_peers(&self, peers: Vec<PeerInfo>) -> BoxFuture<'_, Result<(), SqliteSessionError>> {
426 Box::pin(async move {
427 let mut result = Ok(());
428 let mut extended_peers = Vec::with_capacity(peers.len());
429 for peer in peers {
430 extended_peers.push(match self.peer(peer.id()).await {
431 Ok(Some(mut existing_peer)) => {
432 existing_peer.extend_info(&peer);
433 existing_peer
434 }
435 Ok(None) => peer,
436 Err(e) => {
437 result = Err(e);
438 peer
439 }
440 });
441 }
442
443 let db = self.database.lock().await;
444 let tx = db.0.transaction().await.unwrap();
445 let stmt = tx
446 .prepare("INSERT OR REPLACE INTO peer_info VALUES (:peer_id, :hash, :subtype)")
447 .await
448 .unwrap();
449 for peer in extended_peers {
450 let subtype = match peer {
451 PeerInfo::User { bot, is_self, .. } => {
452 match (bot.unwrap_or_default(), is_self.unwrap_or_default()) {
453 (true, true) => Some(PeerSubtype::UserSelfBot),
454 (true, false) => Some(PeerSubtype::UserBot),
455 (false, true) => Some(PeerSubtype::UserSelf),
456 (false, false) => None,
457 }
458 }
459 PeerInfo::Chat { .. } => None,
460 PeerInfo::Channel { kind, .. } => kind.map(|kind| match kind {
461 ChannelKind::Megagroup => PeerSubtype::Megagroup,
462 ChannelKind::Broadcast => PeerSubtype::Broadcast,
463 ChannelKind::Gigagroup => PeerSubtype::Gigagroup,
464 ChannelKind::Community => PeerSubtype::Community,
465 }),
466 };
467 let mut params = vec![];
468 let peer_id = peer.id().bot_api_dialog_id_unchecked();
469 params.push((":peer_id".to_owned(), Some(peer_id)));
470 params.push((":hash".to_owned(), peer.auth().map(|a| a.hash())));
471 params.push((":subtype".to_owned(), subtype.map(|s| s as i64)));
472 if let Err(err) = stmt.execute(params).await {
473 result = Err(err.into());
474 }
475 stmt.reset();
476 }
477 tx.commit().await?;
478 result
479 })
480 }
481
482 fn updates_state(&self) -> BoxFuture<'_, Result<UpdatesState, SqliteSessionError>> {
483 Box::pin(async move {
484 let db = self.database.lock().await;
485 let mut state = db
486 .fetch_one(
487 "SELECT * FROM update_state LIMIT 1",
488 named_params![],
489 |row| {
490 Ok(UpdatesState {
491 pts: row.get(0)?,
492 qts: row.get(1)?,
493 date: row.get(2)?,
494 seq: row.get(3)?,
495 channels: Vec::new(),
496 })
497 },
498 )
499 .await?
500 .unwrap_or_default();
501 state.channels = db
502 .fetch_all("SELECT * FROM channel_state", named_params![], |row| {
503 Ok(ChannelState {
504 id: row.get(0)?,
505 pts: row.get(1)?,
506 })
507 })
508 .await?;
509 Ok(state)
510 })
511 }
512
513 fn set_update_state(
514 &self,
515 update: UpdateState,
516 ) -> BoxFuture<'_, Result<(), SqliteSessionError>> {
517 Box::pin(async move {
518 let db = self.database.lock().await;
519 let transaction = db.begin_transaction().await?;
520
521 match update {
522 UpdateState::All(updates_state) => {
523 transaction
524 .execute("DELETE FROM update_state", params![])
525 .await?;
526 transaction
527 .execute(
528 "INSERT INTO update_state VALUES (:pts, :qts, :date, :seq)",
529 named_params! {
530 ":pts": updates_state.pts,
531 ":qts": updates_state.qts,
532 ":date": updates_state.date,
533 ":seq": updates_state.seq,
534 },
535 )
536 .await?;
537
538 transaction
539 .execute("DELETE FROM channel_state", params![])
540 .await?;
541 for channel in updates_state.channels {
542 transaction
543 .execute(
544 "INSERT INTO channel_state VALUES (:peer_id, :pts)",
545 named_params! {
546 ":peer_id": channel.id,
547 ":pts": channel.pts,
548 },
549 )
550 .await?;
551 }
552 }
553 UpdateState::Primary { pts, date, seq } => {
554 let previous = db
555 .fetch_one(
556 "SELECT * FROM update_state LIMIT 1",
557 named_params![],
558 |_| Ok(()),
559 )
560 .await?;
561
562 if previous.is_some() {
563 transaction
564 .execute(
565 "UPDATE update_state SET pts = :pts, date = :date, seq = :seq",
566 named_params! {
567 ":pts": pts,
568 ":date": date,
569 ":seq": seq,
570 },
571 )
572 .await?;
573 } else {
574 transaction
575 .execute(
576 "INSERT INTO update_state VALUES (:pts, 0, :date, :seq)",
577 named_params! {
578 ":pts": pts,
579 ":date": date,
580 ":seq": seq,
581 },
582 )
583 .await?;
584 }
585 }
586 UpdateState::Secondary { qts } => {
587 let previous = db
588 .fetch_one(
589 "SELECT * FROM update_state LIMIT 1",
590 named_params![],
591 |_| Ok(()),
592 )
593 .await?;
594
595 if previous.is_some() {
596 transaction
597 .execute(
598 "UPDATE update_state SET qts = :qts",
599 named_params! {":qts": qts},
600 )
601 .await?;
602 } else {
603 transaction
604 .execute(
605 "INSERT INTO update_state VALUES (0, :qts, 0, 0)",
606 named_params! {":qts": qts},
607 )
608 .await?;
609 }
610 }
611 UpdateState::Channel { id, pts } => {
612 transaction
613 .execute(
614 "INSERT OR REPLACE INTO channel_state VALUES (:peer_id, :pts)",
615 named_params! {
616 ":peer_id": id,
617 ":pts": pts,
618 },
619 )
620 .await?;
621 }
622 }
623
624 transaction.commit().await?;
625 Ok(())
626 })
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
633
634 use {DcOption, KNOWN_DC_OPTIONS, PeerInfo, Session, UpdateState};
635
636 use super::*;
637
638 #[test]
639 fn exercise_sqlite_session() {
640 tokio::runtime::Builder::new_current_thread()
641 .enable_all()
642 .build()
643 .unwrap()
644 .block_on(do_exercise_sqlite_session());
645 }
646
647 async fn do_exercise_sqlite_session() {
648 let session = SqliteSession::open(":memory:").await.unwrap();
649
650 assert_eq!(session.home_dc_id().unwrap(), DEFAULT_DC);
651 session.set_home_dc_id(DEFAULT_DC + 1).await.unwrap();
652 assert_eq!(session.home_dc_id().unwrap(), DEFAULT_DC + 1);
653
654 assert_eq!(
655 session.dc_option(KNOWN_DC_OPTIONS[0].id).unwrap(),
656 Some(KNOWN_DC_OPTIONS[0].clone())
657 );
658 let new_dc_option = DcOption {
659 id: KNOWN_DC_OPTIONS
660 .iter()
661 .map(|dc_option| dc_option.id)
662 .max()
663 .unwrap()
664 + 1,
665 ipv4: SocketAddrV4::new(Ipv4Addr::from_bits(0), 1),
666 ipv6: SocketAddrV6::new(Ipv6Addr::from_bits(0), 1, 0, 0),
667 auth_key: Some([1; 256]),
668 };
669 assert_eq!(session.dc_option(new_dc_option.id).unwrap(), None);
670 session.set_dc_option(&new_dc_option).await.unwrap();
671 assert_eq!(
672 session.dc_option(new_dc_option.id).unwrap(),
673 Some(new_dc_option)
674 );
675
676 assert_eq!(session.peer(PeerId::self_user()).await.unwrap(), None);
677 assert_eq!(session.peer(PeerId::user_unchecked(1)).await.unwrap(), None);
678 let peer = PeerInfo::User {
679 id: 1,
680 auth: None,
681 bot: Some(true),
682 is_self: Some(true),
683 };
684 session.cache_peer(peer.clone()).await.unwrap();
685 assert_eq!(
686 session.peer(PeerId::self_user()).await.unwrap(),
687 Some(peer.clone())
688 );
689 assert_eq!(
690 session.peer(PeerId::user_unchecked(1)).await.unwrap(),
691 Some(peer)
692 );
693
694 assert_eq!(
695 session.peer(PeerId::channel_unchecked(1)).await.unwrap(),
696 None
697 );
698 let peer = PeerInfo::Channel {
699 id: 1,
700 auth: Some(PeerAuth::from_hash(-1)),
701 kind: Some(ChannelKind::Broadcast),
702 };
703 session.cache_peer(peer.clone()).await.unwrap();
704 assert_eq!(
705 session.peer(PeerId::channel_unchecked(1)).await.unwrap(),
706 Some(peer)
707 );
708
709 let peers = vec![
710 PeerInfo::Channel {
711 id: 2,
712 auth: Some(PeerAuth::from_hash(-2)),
713 kind: Some(ChannelKind::Megagroup),
714 },
715 PeerInfo::Channel {
716 id: 3,
717 auth: Some(PeerAuth::from_hash(-3)),
718 kind: None,
719 },
720 ];
721 session.cache_peers(peers.clone()).await.unwrap();
722 for peer in peers.iter() {
723 assert_eq!(session.peer(peer.id()).await.unwrap().as_ref(), Some(peer));
724 }
725
726 let new_inserted_peers = vec![
729 PeerInfo::Channel {
730 id: 2,
731 auth: None,
732 kind: Some(ChannelKind::Broadcast),
733 },
734 PeerInfo::Channel {
735 id: 3,
736 auth: Some(PeerAuth::from_hash(-30)),
737 kind: None,
738 },
739 ];
740 session.cache_peers(new_inserted_peers).await.unwrap();
741 assert_eq!(
742 session.peer(PeerId::channel_unchecked(2)).await.unwrap(),
743 Some(PeerInfo::Channel {
744 id: 2,
745 auth: Some(PeerAuth::from_hash(-2)),
746 kind: Some(ChannelKind::Broadcast),
747 })
748 );
749 assert_eq!(
750 session.peer(PeerId::channel_unchecked(3)).await.unwrap(),
751 Some(PeerInfo::Channel {
752 id: 3,
753 auth: Some(PeerAuth::from_hash(-30)),
754 kind: None
755 })
756 );
757
758 assert_eq!(
759 session.updates_state().await.unwrap(),
760 UpdatesState::default()
761 );
762 session
763 .set_update_state(UpdateState::All(UpdatesState {
764 pts: 1,
765 qts: 2,
766 date: 3,
767 seq: 4,
768 channels: vec![
769 ChannelState { id: 5, pts: 6 },
770 ChannelState { id: 7, pts: 8 },
771 ],
772 }))
773 .await
774 .unwrap();
775 session
776 .set_update_state(UpdateState::Primary {
777 pts: 2,
778 date: 4,
779 seq: 5,
780 })
781 .await
782 .unwrap();
783 session
784 .set_update_state(UpdateState::Secondary { qts: 3 })
785 .await
786 .unwrap();
787 session
788 .set_update_state(UpdateState::Channel { id: 7, pts: 9 })
789 .await
790 .unwrap();
791 assert_eq!(
792 session.updates_state().await.unwrap(),
793 UpdatesState {
794 pts: 2,
795 qts: 3,
796 date: 4,
797 seq: 5,
798 channels: vec![
799 ChannelState { id: 5, pts: 6 },
800 ChannelState { id: 7, pts: 9 },
801 ],
802 }
803 );
804 }
805}