1#![cfg_attr(not(test), allow(unused))]
16
17use std::{collections::HashMap, rc::Rc, time::Duration};
18
19use indexed_db_futures::{Build, database::Database};
20#[cfg(target_family = "wasm")]
21use matrix_sdk_base::{
22 cross_process_lock::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION},
23 event_cache::thread::ThreadInfo,
24};
25use matrix_sdk_base::{
26 event_cache::{Event, Gap, store::EventCacheStore},
27 linked_chunk::{
28 ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
29 RawChunk, Update,
30 },
31 timer,
32};
33use ruma::{
34 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType,
35};
36use tracing::{error, instrument, trace};
37use web_sys::IdbTransactionMode;
38
39use crate::{
40 event_cache_store::{
41 migrations::current::keys,
42 transaction::IndexeddbEventCacheStoreTransaction,
43 types::{ChunkType, InBandEvent, Lease, OutOfBandEvent, Thread},
44 },
45 serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed},
46 transaction::TransactionError,
47};
48
49mod builder;
50mod error;
51#[cfg(all(test, target_family = "wasm"))]
52mod integration_tests;
53mod migrations;
54mod serializer;
55mod transaction;
56mod types;
57
58pub use builder::IndexeddbEventCacheStoreBuilder;
59pub use error::IndexeddbEventCacheStoreError;
60
61#[derive(Debug, Clone)]
67pub struct IndexeddbEventCacheStore {
68 inner: Rc<Database>,
70 serializer: IndexedTypeSerializer,
72}
73
74impl IndexeddbEventCacheStore {
75 pub fn builder() -> IndexeddbEventCacheStoreBuilder {
78 IndexeddbEventCacheStoreBuilder::default()
79 }
80
81 pub fn transaction<'a>(
85 &'a self,
86 stores: &[&str],
87 mode: IdbTransactionMode,
88 ) -> Result<IndexeddbEventCacheStoreTransaction<'a>, IndexeddbEventCacheStoreError> {
89 Ok(IndexeddbEventCacheStoreTransaction::new(
90 self.inner
91 .transaction(stores)
92 .with_mode(mode)
93 .build()
94 .map_err(TransactionError::from)?,
95 &self.serializer,
96 ))
97 }
98}
99
100#[cfg(target_family = "wasm")]
101#[async_trait::async_trait(?Send)]
102impl EventCacheStore for IndexeddbEventCacheStore {
103 type Error = IndexeddbEventCacheStoreError;
104
105 #[instrument(skip(self))]
106 async fn try_take_leased_lock(
107 &self,
108 lease_duration_ms: u32,
109 key: &str,
110 holder: &str,
111 ) -> Result<Option<CrossProcessLockGeneration>, IndexeddbEventCacheStoreError> {
112 let transaction =
113 self.transaction(&[Lease::OBJECT_STORE], IdbTransactionMode::Readwrite)?;
114
115 let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());
116 let expiration = now + Duration::from_millis(lease_duration_ms.into());
117
118 let lease = match transaction.get_lease_by_id(key).await? {
119 Some(mut lease) => {
120 if lease.holder == holder {
121 lease.expiration = expiration;
123
124 Some(lease)
125 } else {
126 if lease.expiration < now {
128 lease.holder = holder.to_owned();
130 lease.expiration = expiration;
131 lease.generation += 1;
132
133 Some(lease)
134 } else {
135 None
137 }
138 }
139 }
140 None => {
141 let lease = Lease {
142 key: key.to_owned(),
143 holder: holder.to_owned(),
144 expiration,
145 generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
146 };
147
148 Some(lease)
149 }
150 };
151
152 Ok(if let Some(lease) = lease {
153 transaction.put_lease(&lease)?;
154 transaction.commit().await?;
155
156 Some(lease.generation)
157 } else {
158 None
159 })
160 }
161
162 #[instrument(skip(self, updates))]
163 async fn handle_linked_chunk_updates(
164 &self,
165 linked_chunk_id: LinkedChunkId<'_>,
166 updates: Vec<Update<Event, Gap>>,
167 ) -> Result<(), IndexeddbEventCacheStoreError> {
168 let _timer = timer!("method");
169
170 let transaction = self.transaction(
171 &[keys::LINKED_CHUNKS, keys::GAPS, keys::EVENTS],
172 IdbTransactionMode::Readwrite,
173 )?;
174
175 let is_complete_event = |event: &Event| {
178 let Some(event_id) = event.event_id() else {
179 error!("Found event with no ID");
180 return false;
181 };
182 if event.kind.event_type().is_none() {
183 error!(%event_id, "Found an event with no event type");
184 return false;
185 }
186 true
187 };
188
189 for update in updates {
190 match update {
191 Update::NewItemsChunk { previous, new, next } => {
192 trace!(%linked_chunk_id, "Inserting new chunk (prev={previous:?}, new={new:?}, next={next:?})");
193 transaction
194 .add_chunk(&types::Chunk {
195 linked_chunk_id: linked_chunk_id.to_owned(),
196 identifier: new.index(),
197 previous: previous.map(|i| i.index()),
198 next: next.map(|i| i.index()),
199 chunk_type: ChunkType::Event,
200 })
201 .await?;
202 }
203 Update::NewGapChunk { previous, new, next, gap } => {
204 trace!(%linked_chunk_id, "Inserting new gap (prev={previous:?}, new={new:?}, next={next:?})");
205 transaction.add_item(&types::Gap {
206 linked_chunk_id: linked_chunk_id.to_owned(),
207 chunk_identifier: new.index(),
208 token: gap.token,
209 })?;
210 transaction
211 .add_chunk(&types::Chunk {
212 linked_chunk_id: linked_chunk_id.to_owned(),
213 identifier: new.index(),
214 previous: previous.map(|i| i.index()),
215 next: next.map(|i| i.index()),
216 chunk_type: ChunkType::Gap,
217 })
218 .await?;
219 }
220 Update::RemoveChunk(chunk_id) => {
221 trace!(%linked_chunk_id, "Removing chunk {chunk_id:?}");
222 transaction.delete_chunk_by_id(linked_chunk_id, chunk_id).await?;
223 }
224 Update::PushItems { at, items } => {
225 let chunk_identifier = at.chunk_identifier().index();
226
227 trace!(%linked_chunk_id, "pushing {} items @ {chunk_identifier}", items.len());
228
229 for (i, item) in items.into_iter().filter(is_complete_event).enumerate() {
230 transaction
231 .add_event(&types::Event::InBand(InBandEvent {
232 linked_chunk_id: linked_chunk_id.to_owned(),
233 content: item,
234 position: types::Position {
235 chunk_identifier,
236 index: at.index() + i,
237 },
238 }))
239 .await?;
240 }
241 }
242 Update::ReplaceItem { at, item } => {
243 let chunk_id = at.chunk_identifier().index();
244 let index = at.index();
245
246 trace!(%linked_chunk_id, "replacing item @ {chunk_id}:{index}");
247
248 if !is_complete_event(&item) {
249 continue;
250 }
251
252 transaction
253 .put_event(&types::Event::InBand(InBandEvent {
254 linked_chunk_id: linked_chunk_id.to_owned(),
255 content: item,
256 position: at.into(),
257 }))
258 .await?;
259 }
260 Update::RemoveItem { at } => {
261 let chunk_id = at.chunk_identifier().index();
262 let index = at.index();
263
264 trace!(%linked_chunk_id, "removing item @ {chunk_id}:{index}");
265
266 transaction.delete_event_by_position(linked_chunk_id, at.into()).await?;
267 }
268 Update::DetachLastItems { at } => {
269 let chunk_id = at.chunk_identifier().index();
270 let index = at.index();
271
272 trace!(%linked_chunk_id, "detaching last items @ {chunk_id}:{index}");
273
274 transaction
275 .delete_events_by_chunk_from_index(linked_chunk_id, at.into())
276 .await?;
277 }
278 Update::StartReattachItems | Update::EndReattachItems => {
279 }
281 Update::Clear => {
282 trace!(%linked_chunk_id, "clearing room");
283 transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
284 transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
285 transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
286 }
287 }
288 }
289 transaction.commit().await?;
290 Ok(())
291 }
292
293 #[instrument(skip(self))]
294 async fn load_all_chunks(
295 &self,
296 linked_chunk_id: LinkedChunkId<'_>,
297 ) -> Result<Vec<RawChunk<Event, Gap>>, IndexeddbEventCacheStoreError> {
298 let _ = timer!("method");
299
300 let transaction = self.transaction(
301 &[keys::LINKED_CHUNKS, keys::GAPS, keys::EVENTS],
302 IdbTransactionMode::Readonly,
303 )?;
304
305 let mut raw_chunks = Vec::new();
306 let chunks = transaction.get_chunks_by_linked_chunk_id(linked_chunk_id).await?;
307 for chunk in chunks {
308 if let Some(raw_chunk) = transaction
309 .load_chunk_by_id(linked_chunk_id, ChunkIdentifier::new(chunk.identifier))
310 .await?
311 {
312 raw_chunks.push(raw_chunk);
313 }
314 }
315 Ok(raw_chunks)
316 }
317
318 #[instrument(skip(self))]
319 async fn load_all_chunks_metadata(
320 &self,
321 linked_chunk_id: LinkedChunkId<'_>,
322 ) -> Result<Vec<ChunkMetadata>, IndexeddbEventCacheStoreError> {
323 let _ = timer!("method");
334
335 let transaction = self.transaction(
336 &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
337 IdbTransactionMode::Readonly,
338 )?;
339
340 let mut raw_chunks = Vec::new();
341 let chunks = transaction.get_chunks_by_linked_chunk_id(linked_chunk_id).await?;
342 for chunk in chunks {
343 let chunk_id = ChunkIdentifier::new(chunk.identifier);
344 let num_items =
345 transaction.get_events_count_by_chunk(linked_chunk_id, chunk_id).await?;
346 raw_chunks.push(ChunkMetadata {
347 num_items,
348 previous: chunk.previous.map(ChunkIdentifier::new),
349 identifier: ChunkIdentifier::new(chunk.identifier),
350 next: chunk.next.map(ChunkIdentifier::new),
351 });
352 }
353 Ok(raw_chunks)
354 }
355
356 #[instrument(skip(self))]
357 async fn load_last_chunk(
358 &self,
359 linked_chunk_id: LinkedChunkId<'_>,
360 ) -> Result<
361 (Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator),
362 IndexeddbEventCacheStoreError,
363 > {
364 let _timer = timer!("method");
365
366 let transaction = self.transaction(
367 &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
368 IdbTransactionMode::Readonly,
369 )?;
370
371 if transaction.get_chunks_count_by_linked_chunk_id(linked_chunk_id).await? == 0 {
372 return Ok((None, ChunkIdentifierGenerator::new_from_scratch()));
373 }
374 match transaction.get_chunk_by_next_chunk_id(linked_chunk_id, None).await {
378 Err(TransactionError::ItemIsNotUnique) => {
379 Err(IndexeddbEventCacheStoreError::ChunksContainDisjointLists)
383 }
384 Err(e) => {
385 Err(e.into())
388 }
389 Ok(None) => {
390 Err(IndexeddbEventCacheStoreError::ChunksContainCycle)
394 }
395 Ok(Some(last_chunk)) => {
396 let last_chunk_identifier = ChunkIdentifier::new(last_chunk.identifier);
397
398 let (last_raw_chunk, max_chunk_id) = futures_util::future::try_join(
399 transaction.load_chunk_by_id(linked_chunk_id, last_chunk_identifier),
400 transaction.get_max_chunk_by_id(linked_chunk_id),
401 )
402 .await?;
403
404 let last_raw_chunk =
405 last_raw_chunk.ok_or(IndexeddbEventCacheStoreError::UnableToLoadChunk)?;
406 let max_chunk_id = max_chunk_id
407 .map(|chunk| ChunkIdentifier::new(chunk.identifier))
408 .ok_or(IndexeddbEventCacheStoreError::NoMaxChunkId)?;
409 let generator =
410 ChunkIdentifierGenerator::new_from_previous_chunk_identifier(max_chunk_id);
411 Ok((Some(last_raw_chunk), generator))
412 }
413 }
414 }
415
416 #[instrument(skip(self))]
417 async fn load_previous_chunk(
418 &self,
419 linked_chunk_id: LinkedChunkId<'_>,
420 before_chunk_identifier: ChunkIdentifier,
421 ) -> Result<Option<RawChunk<Event, Gap>>, IndexeddbEventCacheStoreError> {
422 let _timer = timer!("method");
423
424 let transaction = self.transaction(
425 &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
426 IdbTransactionMode::Readonly,
427 )?;
428 if let Some(chunk) =
429 transaction.get_chunk_by_id(linked_chunk_id, before_chunk_identifier).await?
430 && let Some(previous_identifier) = chunk.previous
431 {
432 let previous_identifier = ChunkIdentifier::new(previous_identifier);
433 Ok(transaction.load_chunk_by_id(linked_chunk_id, previous_identifier).await?)
434 } else {
435 Ok(None)
436 }
437 }
438
439 #[instrument(skip(self))]
440 async fn load_thread_info(
441 &self,
442 room_id: &RoomId,
443 thread_id: &EventId,
444 ) -> Result<ThreadInfo, Self::Error> {
445 let _timer = timer!("method");
446
447 let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readonly)?;
448
449 if let Some(thread) = transaction.load_thread_info(room_id, thread_id).await? {
450 return Ok(thread.info);
451 }
452
453 drop(transaction);
454
455 let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
456
457 let thread = Thread {
458 room_id: room_id.to_owned(),
459 thread_id: thread_id.to_owned(),
460 info: ThreadInfo::new(),
461 };
462 transaction.update_thread_info(&thread)?;
463 transaction.commit().await?;
464
465 Ok(thread.info)
466 }
467
468 #[instrument(skip(self))]
469 async fn update_thread_info(
470 &self,
471 room_id: &RoomId,
472 thread_id: &EventId,
473 thread_info: &ThreadInfo,
474 ) -> Result<(), Self::Error> {
475 let _timer = timer!("method");
476
477 let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
478
479 let thread = Thread {
480 room_id: room_id.to_owned(),
481 thread_id: thread_id.to_owned(),
482 info: thread_info.clone(),
483 };
484 transaction.update_thread_info(&thread)?;
485 transaction.commit().await?;
486
487 Ok(())
488 }
489
490 #[instrument(skip(self))]
491 async fn clear_all_events(
492 &self,
493 room_id: Option<&RoomId>,
494 ) -> Result<(), IndexeddbEventCacheStoreError> {
495 let _timer = timer!("method");
496
497 let transaction = self.transaction(
498 &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS, keys::THREADS],
499 IdbTransactionMode::Readwrite,
500 )?;
501
502 match room_id {
503 None => {
505 transaction.clear::<types::Chunk>()?;
506 transaction.clear::<types::Event>()?;
507 transaction.clear::<types::Gap>()?;
508 transaction.commit().await?;
509 }
510
511 Some(room_id) => {
513 {
515 for linked_chunk_id in
516 [LinkedChunkId::Room(room_id), LinkedChunkId::PinnedEvents(room_id)]
517 {
518 transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
521 transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
522 transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
523 }
524 }
525
526 {
528 for thread in transaction.get_threads_by_room_id(room_id).await? {
529 let linked_chunk_id = thread.linked_chunk();
530
531 transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
534 transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
535 transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
536 }
537 }
538
539 transaction.commit().await?;
541 }
542 }
543 Ok(())
544 }
545
546 #[instrument(skip(self, events))]
547 async fn filter_duplicated_events(
548 &self,
549 linked_chunk_id: LinkedChunkId<'_>,
550 events: Vec<OwnedEventId>,
551 ) -> Result<Vec<(OwnedEventId, Position)>, IndexeddbEventCacheStoreError> {
552 let _timer = timer!("method");
553
554 if events.is_empty() {
555 return Ok(Vec::new());
556 }
557
558 let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
559 let mut duplicated = Vec::new();
560 for event_id in events {
561 if let Some(types::Event::InBand(event)) =
562 transaction.get_event_by_id(linked_chunk_id, &event_id).await?
563 {
564 duplicated.push((event_id, event.position.into()));
565 }
566 }
567 Ok(duplicated)
568 }
569
570 #[instrument(skip(self, event_id))]
571 async fn find_event(
572 &self,
573 room_id: &RoomId,
574 event_id: &EventId,
575 ) -> Result<Option<Event>, IndexeddbEventCacheStoreError> {
576 let _timer = timer!("method");
577
578 let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
579 transaction
580 .get_events_by_room(room_id, event_id)
581 .await
582 .map(|mut events| events.pop().map(Into::into))
583 .map_err(Into::into)
584 }
585
586 #[instrument(skip(self, event_id, filters))]
587 async fn find_event_relations(
588 &self,
589 room_id: &RoomId,
590 event_id: &EventId,
591 filters: Option<&[RelationType]>,
592 ) -> Result<Vec<(Event, Option<Position>)>, IndexeddbEventCacheStoreError> {
593 let _timer = timer!("method");
594
595 let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
596
597 let mut related_events = HashMap::<OwnedEventId, types::Event>::new();
598 match filters {
599 Some(relation_types) if !relation_types.is_empty() => {
600 for relation_type in relation_types {
601 let relation = (event_id, relation_type);
602 let events = transaction.get_events_by_relation(room_id, relation).await?;
603 for event in events {
604 let Some(event_id) = event.event_id() else {
605 return Err(IndexeddbEventCacheStoreError::EventWithoutId);
606 };
607 match event.linked_chunk_id() {
608 LinkedChunkId::Room(_) => {
609 related_events.insert(event_id.to_owned(), event);
612 }
613 _ => {
614 related_events
617 .entry(event_id.to_owned())
618 .or_insert_with(|| event.into_out_of_band_event());
619 }
620 }
621 }
622 }
623 }
624 _ => {
625 for event in transaction.get_events_by_related_event(room_id, event_id).await? {
626 let Some(event_id) = event.event_id() else {
627 return Err(IndexeddbEventCacheStoreError::EventWithoutId);
628 };
629 match event.linked_chunk_id() {
630 LinkedChunkId::Room(_) => {
631 related_events.insert(event_id.to_owned(), event);
634 }
635 _ => {
636 related_events
639 .entry(event_id.to_owned())
640 .or_insert_with(|| event.into_out_of_band_event());
641 }
642 }
643 }
644 }
645 }
646 Ok(related_events
647 .into_values()
648 .map(|event| {
649 let position = event.position().map(Into::into);
650 (event.into(), position)
651 })
652 .collect())
653 }
654
655 #[instrument(skip(self))]
656 async fn get_room_events(
657 &self,
658 room_id: &RoomId,
659 event_type: Option<&str>,
660 session_id: Option<&str>,
661 ) -> Result<Vec<Event>, IndexeddbEventCacheStoreError> {
662 let _timer = timer!("method");
663
664 let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
668 transaction
669 .get_room_events(room_id)
670 .await
671 .map(|mut vec| {
672 vec.dedup_by(|a, b| {
673 if let (Some(a), Some(b)) = (a.event_id(), b.event_id()) {
674 a == b
675 } else {
676 false
677 }
678 });
679 vec.into_iter()
680 .map(Event::from)
681 .filter(|e| {
682 event_type.is_none_or(|event_type| {
683 Some(event_type) == e.kind.event_type().as_deref()
684 })
685 })
686 .filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
687 .collect()
688 })
689 .map_err(Into::into)
690 }
691
692 #[instrument(skip(self, event))]
693 async fn save_event(
694 &self,
695 room_id: &RoomId,
696 event: Event,
697 ) -> Result<(), IndexeddbEventCacheStoreError> {
698 let _timer = timer!("method");
699
700 let Some(event_id) = event.event_id() else {
701 error!(%room_id, "Trying to save an event with no ID");
702 return Ok(());
703 };
704 let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readwrite)?;
705
706 let mut events = transaction
707 .get_events_by_room(room_id, event_id)
708 .await?
709 .into_iter()
710 .map(|e| e.with_content(event.clone()))
711 .collect::<Vec<types::Event>>();
712 if events.is_empty() {
713 events.push(types::Event::OutOfBand(OutOfBandEvent {
714 linked_chunk_id: LinkedChunkId::Room(room_id).to_owned(),
715 content: event,
716 position: (),
717 }));
718 }
719 for event in events {
720 transaction.put_event(&event).await?;
721 }
722 transaction.commit().await?;
723 Ok(())
724 }
725
726 async fn optimize(&self) -> Result<(), Self::Error> {
727 Ok(())
728 }
729
730 async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
731 Ok(None)
732 }
733
734 async fn close(&self) -> Result<(), Self::Error> {
735 Ok(())
736 }
737
738 async fn reopen(&self) -> Result<(), Self::Error> {
739 Ok(())
740 }
741}
742
743#[cfg(all(test, target_family = "wasm"))]
744mod tests {
745 use matrix_sdk_base::{
746 event_cache::store::EventCacheStoreError, event_cache_store_integration_tests,
747 event_cache_store_integration_tests_time,
748 };
749 use uuid::Uuid;
750
751 use crate::{
752 event_cache_store::IndexeddbEventCacheStore, indexeddb_event_cache_store_integration_tests,
753 };
754
755 mod unencrypted {
756 use super::*;
757
758 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
759
760 async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
761 let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
762 Ok(IndexeddbEventCacheStore::builder().database_name(name).build().await?)
763 }
764
765 event_cache_store_integration_tests!();
766 event_cache_store_integration_tests_time!();
767
768 indexeddb_event_cache_store_integration_tests!();
769 }
770
771 mod encrypted {
772 use std::sync::Arc;
773
774 use matrix_sdk_store_encryption::StoreCipher;
775
776 use super::*;
777
778 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
779
780 async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
781 let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
782 Ok(IndexeddbEventCacheStore::builder()
783 .database_name(name)
784 .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
785 .build()
786 .await?)
787 }
788
789 event_cache_store_integration_tests!();
790 event_cache_store_integration_tests_time!();
791
792 indexeddb_event_cache_store_integration_tests!();
793 }
794}