Skip to main content

inline_client/
store.rs

1//! Store boundary for durable client state.
2//!
3//! The built-in SQLite implementation persists sessions, sync cursors and
4//! journals, dialogs, users, messages, reconciliation state, and transactions.
5//! The trait keeps storage pluggable for hosts with platform-specific keychain
6//! or database requirements.
7
8use std::{
9    collections::{HashMap, HashSet},
10    fmt,
11    fs::OpenOptions,
12    path::Path,
13    sync::{Arc, Mutex},
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17use futures_util::future::BoxFuture;
18use rusqlite::{Connection, OptionalExtension, Row, params, types::Type};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22    AuthCredential, ChatParticipantRecord, ClientErrorCategory, ClientEvent, ClientFailure,
23    DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsOrder, DialogsPage,
24    DialogsRequest, EventReliability, HistoryPage, HistoryRequest, InlineId, MessageContent,
25    MessageRecord, SpaceMemberRecord, SpaceRecord, TransactionEvent, TransactionId,
26    TransactionIdentity, TransactionState, UserRecord, UserSettingsRecord,
27};
28
29/// Selects a history page by the monotonic message-ID cursor before applying
30/// presentation ordering. Keeping this shared prevents timestamp skew from
31/// changing which records belong to a cursor window.
32pub(crate) fn select_history_window(
33    mut messages: Vec<MessageRecord>,
34    limit: usize,
35    before_message_id: Option<InlineId>,
36    after_message_id: Option<InlineId>,
37) -> (Vec<MessageRecord>, bool) {
38    if let Some(before) = before_message_id {
39        messages.retain(|message| message.message_id.get() < before.get());
40    }
41    if let Some(after) = after_message_id {
42        messages.retain(|message| message.message_id.get() > after.get());
43    }
44
45    messages.sort_by_key(|message| message.message_id.get());
46    let limit = limit.max(1);
47    let has_more = messages.len() > limit;
48    if has_more {
49        if after_message_id.is_some() {
50            messages.truncate(limit);
51        } else {
52            messages = messages.split_off(messages.len() - limit);
53        }
54    }
55    messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
56    (messages, has_more)
57}
58
59/// Result type returned by client stores.
60pub type StoreResult<T> = Result<T, StoreError>;
61
62/// Redacted store error.
63#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
64#[error("{category:?}: {message}")]
65pub struct StoreError {
66    /// Stable error category.
67    pub category: ClientErrorCategory,
68    /// Redacted message suitable for hosts.
69    pub message: String,
70}
71
72impl StoreError {
73    /// Creates a store error.
74    pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
75        Self {
76            category,
77            message: message.into(),
78        }
79    }
80
81    fn internal(message: impl Into<String>) -> Self {
82        Self::new(ClientErrorCategory::Internal, message)
83    }
84}
85
86/// Stored session metadata.
87#[derive(Clone, PartialEq, Eq)]
88pub struct StoredSession {
89    /// Auth credential needed by the runtime.
90    pub auth: AuthCredential,
91    /// Optional account/store namespace chosen by the host.
92    pub account_namespace: Option<String>,
93}
94
95/// Durable global synchronization state.
96///
97/// `last_sync_date` is the newest server update date that the client has
98/// coherently applied. It is used as the discovery cursor for
99/// `getUpdatesState` after startup and reconnect.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct SyncState {
102    /// Latest coherently applied server update date, in Unix seconds.
103    pub last_sync_date: i64,
104}
105
106/// Peer identity for an Inline chat update bucket.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub enum SyncBucketPeer {
109    /// Direct-message bucket addressed by user ID.
110    User {
111        /// Inline user ID.
112        user_id: InlineId,
113    },
114    /// Group or thread bucket addressed by chat ID.
115    Chat {
116        /// Inline chat ID.
117        chat_id: InlineId,
118    },
119}
120
121/// Stable identity of an Inline update bucket.
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
123pub enum SyncBucketKey {
124    /// Updates scoped to the authenticated user.
125    User,
126    /// Updates scoped to an Inline space.
127    Space {
128        /// Inline space ID.
129        space_id: InlineId,
130    },
131    /// Updates scoped to a direct-message or chat peer.
132    Chat {
133        /// Peer that owns the chat bucket.
134        peer: SyncBucketPeer,
135    },
136}
137
138/// Durable cursor for one Inline update bucket.
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
140pub struct SyncBucketState {
141    /// Last coherently applied sequence for the bucket.
142    pub seq: i64,
143    /// Newest coherently applied update date for the bucket, in Unix seconds.
144    pub date: i64,
145}
146
147/// Durable write-ahead record for one fetched update batch. The opaque payload
148/// is owned by the sync engine and retained until reducer writes and the bucket
149/// cursor have both committed.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct PendingSyncBatch {
152    /// Bucket receiving the batch.
153    pub key: SyncBucketKey,
154    /// Cursor to commit after the batch is reapplied successfully.
155    pub committed_state: SyncBucketState,
156    /// Versioned opaque encoded updates and sidecars.
157    pub payload: Vec<u8>,
158}
159
160/// One client event prepared for delivery to the single lossless consumer.
161///
162/// Lossless events have a durable positive `delivery_id`. Best-effort events
163/// are carried through the same ordered batch with no durable identifier.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct ClientEventDelivery {
166    /// Store-local identifier used to acknowledge a durable lossless event.
167    pub delivery_id: Option<u64>,
168    /// Committed client event.
169    pub event: ClientEvent,
170}
171
172impl ClientEventDelivery {
173    /// Creates an event that does not require durable acknowledgement.
174    pub fn ephemeral(event: ClientEvent) -> Self {
175        Self {
176            delivery_id: None,
177            event,
178        }
179    }
180}
181
182/// Durable current reaction state for one user and message.
183#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct StoredReaction {
185    /// Inline chat ID.
186    pub chat_id: InlineId,
187    /// Inline message ID.
188    pub message_id: InlineId,
189    /// Inline user ID that owns the reaction.
190    pub user_id: InlineId,
191    /// Reaction emoji.
192    pub reaction: String,
193}
194
195/// Durable read state for one chat.
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub struct StoredReadState {
198    /// Inline chat ID.
199    pub chat_id: InlineId,
200    /// Highest message ID known read, when supplied by the server.
201    pub read_max_id: Option<InlineId>,
202    /// Remaining unread count, when supplied by the server.
203    pub unread_count: Option<u32>,
204    /// Whether the dialog is explicitly marked unread.
205    pub marked_unread: bool,
206}
207
208/// Durable account-level state needed to reconcile a consumer after an event
209/// stream retention gap.
210#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
211pub struct AccountStateSnapshot {
212    /// Chats that were removed after being observed by this account store.
213    pub deleted_chat_ids: Vec<InlineId>,
214}
215
216/// Durable per-chat state that complements paged dialog/history queries during
217/// reconciliation.
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ChatStateSnapshot {
220    /// Inline chat ID represented by this snapshot.
221    pub chat_id: InlineId,
222    /// Current dialog metadata, or `None` if the chat is deleted or unknown.
223    pub dialog: Option<DialogRecord>,
224    /// Whether the store has a durable chat deletion tombstone.
225    pub deleted: bool,
226    /// Message IDs known to have been deleted from this chat.
227    pub deleted_message_ids: Vec<InlineId>,
228    /// Current reaction set known for the chat.
229    pub reactions: Vec<StoredReaction>,
230    /// Message IDs whose reaction set was observed as a complete snapshot,
231    /// including messages with zero reactions.
232    pub reaction_snapshot_message_ids: Vec<InlineId>,
233    /// Current read state known for the chat.
234    pub read_state: Option<StoredReadState>,
235    /// Current direct participant snapshot known for the chat.
236    pub participants: Vec<ChatParticipantRecord>,
237    /// Whether `participants` is a complete snapshot rather than only deltas.
238    pub participants_complete: bool,
239}
240
241impl fmt::Debug for StoredSession {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        f.debug_struct("StoredSession")
244            .field("auth", &self.auth)
245            .field(
246                "account_namespace",
247                &self.account_namespace.as_ref().map(|_| "<redacted>"),
248            )
249            .finish()
250    }
251}
252
253/// Durable store boundary for client state.
254pub trait ClientStore: fmt::Debug + Send + Sync + 'static {
255    /// Saves the current session.
256    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>>;
257
258    /// Loads the current session.
259    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>>;
260
261    /// Clears the current session.
262    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>>;
263
264    /// Clears all account-owned cache, sync, and transaction state while
265    /// leaving the session row under separate lifecycle control.
266    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>>;
267
268    /// Loads the global synchronization discovery cursor.
269    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>>;
270
271    /// Saves the global synchronization discovery cursor.
272    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>>;
273
274    /// Clears the global synchronization cursor and all bucket cursors.
275    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>>;
276
277    /// Loads the cursor for one update bucket.
278    fn sync_bucket_state(
279        &self,
280        key: SyncBucketKey,
281    ) -> BoxFuture<'static, StoreResult<SyncBucketState>>;
282
283    /// Saves the cursor for one update bucket.
284    fn save_sync_bucket_state(
285        &self,
286        key: SyncBucketKey,
287        state: SyncBucketState,
288    ) -> BoxFuture<'static, StoreResult<()>>;
289
290    /// Saves multiple bucket cursors atomically when supported by the store.
291    fn save_sync_bucket_states(
292        &self,
293        states: Vec<(SyncBucketKey, SyncBucketState)>,
294    ) -> BoxFuture<'static, StoreResult<()>>;
295
296    /// Removes the cursor for one update bucket.
297    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>>;
298
299    /// Saves or replaces the write-ahead batch for one bucket before reducer
300    /// state is changed.
301    fn save_pending_sync_batch(
302        &self,
303        batch: PendingSyncBatch,
304    ) -> BoxFuture<'static, StoreResult<()>>;
305
306    /// Lists write-ahead batches that were not fully committed.
307    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>>;
308
309    /// Removes an invalid write-ahead batch without advancing its bucket
310    /// cursor. Stores should implement this so a journal written by an older
311    /// or incompatible client cannot permanently poison startup recovery.
312    fn discard_pending_sync_batch(
313        &self,
314        _key: SyncBucketKey,
315    ) -> BoxFuture<'static, StoreResult<()>> {
316        Box::pin(async {
317            Err(StoreError::new(
318                ClientErrorCategory::Unsupported,
319                "store does not support discarding invalid sync journals",
320            ))
321        })
322    }
323
324    /// Atomically advances a bucket cursor and removes its write-ahead batch.
325    fn commit_pending_sync_batch(
326        &self,
327        key: SyncBucketKey,
328        state: SyncBucketState,
329    ) -> BoxFuture<'static, StoreResult<()>>;
330
331    /// Atomically advances a bucket cursor, records resulting lossless events,
332    /// and removes its write-ahead batch. Custom stores must override this to
333    /// use durable lossless delivery; the default fails before cursor commit.
334    fn commit_pending_sync_batch_with_events(
335        &self,
336        key: SyncBucketKey,
337        state: SyncBucketState,
338        events: Vec<ClientEvent>,
339    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
340        if events
341            .iter()
342            .any(|event| event.reliability() == EventReliability::Lossless)
343        {
344            return Box::pin(async {
345                Err(StoreError::new(
346                    ClientErrorCategory::Unsupported,
347                    "client store does not support a durable event outbox",
348                ))
349            });
350        }
351        let commit = self.commit_pending_sync_batch(key, state);
352        Box::pin(async move {
353            commit.await?;
354            Ok(events
355                .into_iter()
356                .map(ClientEventDelivery::ephemeral)
357                .collect())
358        })
359    }
360
361    /// Appends committed events to the durable lossless outbox while retaining
362    /// best-effort events as unsequenced deliveries.
363    fn append_client_events(
364        &self,
365        events: Vec<ClientEvent>,
366    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
367        Box::pin(async move {
368            if events
369                .iter()
370                .any(|event| event.reliability() == EventReliability::Lossless)
371            {
372                return Err(StoreError::new(
373                    ClientErrorCategory::Unsupported,
374                    "client store does not support a durable event outbox",
375                ));
376            }
377            Ok(events
378                .into_iter()
379                .map(ClientEventDelivery::ephemeral)
380                .collect())
381        })
382    }
383
384    /// Lists durable lossless events that have not yet been acknowledged by
385    /// the host consumer.
386    fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
387        Box::pin(async { Ok(Vec::new()) })
388    }
389
390    /// Acknowledges one durable lossless event after the host has persisted or
391    /// otherwise committed its handling.
392    fn acknowledge_client_event(&self, _delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
393        Box::pin(async { Ok(()) })
394    }
395
396    /// Lists stored dialogs.
397    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>>;
398
399    /// Loads one stored dialog by chat ID.
400    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>>;
401
402    /// Records a dialog in local state.
403    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>>;
404
405    /// Removes a dialog and its client-owned per-chat state.
406    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>>;
407
408    /// Lists durable chat deletion tombstones.
409    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
410
411    /// Records user summaries in local state.
412    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>>;
413
414    /// Loads one stored user summary.
415    fn user(&self, _user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
416        Box::pin(async { Ok(None) })
417    }
418
419    /// Inserts or replaces a durable space summary.
420    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>>;
421
422    /// Loads a durable space summary.
423    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>>;
424
425    /// Inserts or replaces one durable space member.
426    fn record_space_member(&self, member: SpaceMemberRecord)
427    -> BoxFuture<'static, StoreResult<()>>;
428
429    /// Replaces the complete durable membership snapshot for one space.
430    fn record_space_members(
431        &self,
432        space_id: InlineId,
433        members: Vec<SpaceMemberRecord>,
434    ) -> BoxFuture<'static, StoreResult<()>>;
435
436    /// Removes one durable space member.
437    fn remove_space_member(
438        &self,
439        space_id: InlineId,
440        user_id: InlineId,
441    ) -> BoxFuture<'static, StoreResult<()>>;
442
443    /// Lists durable members for a space.
444    fn space_members(
445        &self,
446        space_id: InlineId,
447    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>>;
448
449    /// Replaces durable global user settings.
450    fn record_user_settings(
451        &self,
452        settings: UserSettingsRecord,
453    ) -> BoxFuture<'static, StoreResult<()>>;
454
455    /// Loads durable global user settings.
456    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>>;
457
458    /// Replaces the durable participant snapshot for a chat.
459    fn record_chat_participants(
460        &self,
461        chat_id: InlineId,
462        participants: Vec<ChatParticipantRecord>,
463    ) -> BoxFuture<'static, StoreResult<()>>;
464
465    /// Inserts or replaces one durable chat participant.
466    fn record_chat_participant(
467        &self,
468        chat_id: InlineId,
469        participant: ChatParticipantRecord,
470    ) -> BoxFuture<'static, StoreResult<()>>;
471
472    /// Removes one durable chat participant.
473    fn remove_chat_participant(
474        &self,
475        chat_id: InlineId,
476        user_id: InlineId,
477    ) -> BoxFuture<'static, StoreResult<()>>;
478
479    /// Loads the durable participant snapshot for a chat.
480    fn chat_participants(
481        &self,
482        chat_id: InlineId,
483    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>>;
484
485    /// Returns whether the participant list was observed as a complete snapshot.
486    fn chat_participants_complete(
487        &self,
488        chat_id: InlineId,
489    ) -> BoxFuture<'static, StoreResult<bool>>;
490
491    /// Fetches stored history.
492    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>>;
493
494    /// Records a message in stored history.
495    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>>;
496
497    /// Records a durable message tombstone and removes the live message row.
498    fn record_message_deleted(
499        &self,
500        chat_id: InlineId,
501        message_id: InlineId,
502    ) -> BoxFuture<'static, StoreResult<()>>;
503
504    /// Returns whether a message has a durable deletion tombstone.
505    fn message_deleted(
506        &self,
507        chat_id: InlineId,
508        message_id: InlineId,
509    ) -> BoxFuture<'static, StoreResult<bool>>;
510
511    /// Lists durable message deletion tombstones for one chat.
512    fn deleted_message_ids(
513        &self,
514        chat_id: InlineId,
515    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
516
517    /// Clears stored messages for one chat and returns the removed IDs.
518    fn clear_chat_messages(
519        &self,
520        chat_id: InlineId,
521        before_date: Option<i64>,
522    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
523
524    /// Lists chat IDs currently associated with a space.
525    fn chat_ids_in_space(
526        &self,
527        space_id: InlineId,
528    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
529
530    /// Inserts or replaces one current reaction.
531    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
532
533    /// Removes one current reaction.
534    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
535
536    /// Replaces the complete reaction set for one message.
537    fn replace_message_reactions(
538        &self,
539        chat_id: InlineId,
540        message_id: InlineId,
541        reactions: Vec<StoredReaction>,
542    ) -> BoxFuture<'static, StoreResult<()>>;
543
544    /// Lists current reactions for a message.
545    fn reactions(
546        &self,
547        chat_id: InlineId,
548        message_id: InlineId,
549    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
550
551    /// Lists the current reaction set for all known messages in one chat.
552    fn reactions_for_chat(
553        &self,
554        chat_id: InlineId,
555    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
556
557    /// Lists message IDs whose reaction set is known to be complete.
558    fn reaction_snapshot_message_ids(
559        &self,
560        chat_id: InlineId,
561    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
562
563    /// Inserts or replaces read state for a chat.
564    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>>;
565
566    /// Loads read state for a chat.
567    fn read_state(
568        &self,
569        chat_id: InlineId,
570    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>>;
571
572    /// Records a transaction state change.
573    fn record_transaction(
574        &self,
575        transaction: StoredTransaction,
576    ) -> BoxFuture<'static, StoreResult<()>>;
577
578    /// Finds a transaction by transaction ID.
579    fn transaction(
580        &self,
581        transaction_id: TransactionId,
582    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>>;
583}
584
585/// Stored durable transaction state.
586#[derive(Clone, Debug, PartialEq, Eq)]
587pub struct StoredTransaction {
588    /// Mutation identity and reconciliation IDs.
589    pub identity: TransactionIdentity,
590    /// Current transaction state.
591    pub state: TransactionState,
592    /// Chat containing the transaction, when known.
593    pub chat_id: Option<InlineId>,
594    /// Final message ID, when known.
595    pub message_id: Option<InlineId>,
596    /// Redacted failure for failed transactions.
597    pub failure: Option<ClientFailure>,
598    /// Unix timestamp in seconds when the transaction row was created.
599    pub created_at: i64,
600    /// Unix timestamp in seconds when the transaction row was last updated.
601    pub updated_at: i64,
602}
603
604impl StoredTransaction {
605    /// Creates a stored transaction in the provided state.
606    pub fn new(identity: TransactionIdentity, state: TransactionState) -> Self {
607        let now = now_seconds();
608        Self {
609            identity,
610            state,
611            chat_id: None,
612            message_id: None,
613            failure: None,
614            created_at: now,
615            updated_at: now,
616        }
617    }
618
619    /// Sets the chat ID.
620    pub fn with_chat_id(mut self, chat_id: InlineId) -> Self {
621        self.chat_id = Some(chat_id);
622        self
623    }
624
625    /// Sets the final message ID.
626    pub fn with_message_id(mut self, message_id: InlineId) -> Self {
627        self.message_id = Some(message_id);
628        self.identity.final_message_id = Some(message_id);
629        self
630    }
631
632    /// Sets the redacted failure.
633    pub fn with_failure(mut self, failure: ClientFailure) -> Self {
634        self.failure = Some(failure);
635        self
636    }
637
638    /// Converts to a client event payload.
639    pub fn event(&self) -> TransactionEvent {
640        TransactionEvent {
641            identity: self.identity.clone(),
642            state: self.state,
643            failure: self.failure.clone(),
644        }
645    }
646}
647
648/// In-memory store for tests and early client development.
649#[derive(Clone, Debug, Default)]
650pub struct InMemoryStore {
651    state: Arc<Mutex<InMemoryStoreState>>,
652}
653
654#[derive(Debug, Default)]
655struct InMemoryStoreState {
656    session: Option<StoredSession>,
657    sync_state: SyncState,
658    sync_buckets: HashMap<SyncBucketKey, SyncBucketState>,
659    pending_sync_batches: HashMap<SyncBucketKey, PendingSyncBatch>,
660    client_event_outbox: Vec<ClientEventDelivery>,
661    next_client_event_id: u64,
662    dialogs: Vec<DialogRecord>,
663    chat_tombstones: HashSet<i64>,
664    users: HashMap<i64, UserRecord>,
665    spaces: HashMap<i64, SpaceRecord>,
666    space_members: HashMap<(i64, i64), SpaceMemberRecord>,
667    user_settings: Option<UserSettingsRecord>,
668    participants: HashMap<i64, HashMap<i64, ChatParticipantRecord>>,
669    participant_snapshots: HashSet<i64>,
670    messages: HashMap<i64, Vec<MessageRecord>>,
671    message_tombstones: HashSet<(i64, i64)>,
672    reactions: HashMap<(i64, i64, i64, String), StoredReaction>,
673    reaction_snapshots: HashSet<(i64, i64)>,
674    read_states: HashMap<i64, StoredReadState>,
675    transactions: HashMap<String, StoredTransaction>,
676}
677
678fn append_memory_client_events(
679    state: &mut InMemoryStoreState,
680    events: Vec<ClientEvent>,
681) -> Vec<ClientEventDelivery> {
682    events
683        .into_iter()
684        .map(|event| {
685            if event.reliability() == EventReliability::BestEffort {
686                return ClientEventDelivery::ephemeral(event);
687            }
688            state.next_client_event_id = state.next_client_event_id.saturating_add(1).max(1);
689            let delivery = ClientEventDelivery {
690                delivery_id: Some(state.next_client_event_id),
691                event,
692            };
693            state.client_event_outbox.push(delivery.clone());
694            delivery
695        })
696        .collect()
697}
698
699impl InMemoryStore {
700    /// Creates an empty in-memory store.
701    pub fn new() -> Self {
702        Self::default()
703    }
704
705    /// Inserts or replaces a dialog.
706    pub fn upsert_dialog(&self, dialog: DialogRecord) {
707        let mut state = self.state.lock().expect("in-memory store poisoned");
708        state.chat_tombstones.remove(&dialog.chat_id.get());
709        upsert_dialog(&mut state.dialogs, dialog);
710    }
711
712    /// Inserts or replaces a user summary.
713    pub fn upsert_user(&self, user: UserRecord) {
714        let mut state = self.state.lock().expect("in-memory store poisoned");
715        state.users.insert(user.user_id.get(), user);
716    }
717
718    /// Inserts a message into history.
719    pub fn insert_message(&self, message: MessageRecord) {
720        let mut state = self.state.lock().expect("in-memory store poisoned");
721        state
722            .message_tombstones
723            .remove(&(message.chat_id.get(), message.message_id.get()));
724        insert_message(&mut state.messages, message);
725    }
726}
727
728impl ClientStore for InMemoryStore {
729    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
730        let store = self.clone();
731        Box::pin(async move {
732            store
733                .state
734                .lock()
735                .expect("in-memory store poisoned")
736                .session = Some(session);
737            Ok(())
738        })
739    }
740
741    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
742        let store = self.clone();
743        Box::pin(async move {
744            Ok(store
745                .state
746                .lock()
747                .expect("in-memory store poisoned")
748                .session
749                .clone())
750        })
751    }
752
753    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
754        let store = self.clone();
755        Box::pin(async move {
756            store
757                .state
758                .lock()
759                .expect("in-memory store poisoned")
760                .session = None;
761            Ok(())
762        })
763    }
764
765    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
766        let store = self.clone();
767        Box::pin(async move {
768            let mut state = store.state.lock().expect("in-memory store poisoned");
769            let session = state.session.take();
770            *state = InMemoryStoreState::default();
771            state.session = session;
772            Ok(())
773        })
774    }
775
776    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
777        let store = self.clone();
778        Box::pin(async move {
779            Ok(store
780                .state
781                .lock()
782                .expect("in-memory store poisoned")
783                .sync_state)
784        })
785    }
786
787    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
788        let store = self.clone();
789        Box::pin(async move {
790            store
791                .state
792                .lock()
793                .expect("in-memory store poisoned")
794                .sync_state = state;
795            Ok(())
796        })
797    }
798
799    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
800        let store = self.clone();
801        Box::pin(async move {
802            let mut state = store.state.lock().expect("in-memory store poisoned");
803            state.sync_state = SyncState::default();
804            state.sync_buckets.clear();
805            Ok(())
806        })
807    }
808
809    fn sync_bucket_state(
810        &self,
811        key: SyncBucketKey,
812    ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
813        let store = self.clone();
814        Box::pin(async move {
815            Ok(store
816                .state
817                .lock()
818                .expect("in-memory store poisoned")
819                .sync_buckets
820                .get(&key)
821                .copied()
822                .unwrap_or_default())
823        })
824    }
825
826    fn save_sync_bucket_state(
827        &self,
828        key: SyncBucketKey,
829        state: SyncBucketState,
830    ) -> BoxFuture<'static, StoreResult<()>> {
831        let store = self.clone();
832        Box::pin(async move {
833            store
834                .state
835                .lock()
836                .expect("in-memory store poisoned")
837                .sync_buckets
838                .insert(key, state);
839            Ok(())
840        })
841    }
842
843    fn save_sync_bucket_states(
844        &self,
845        states: Vec<(SyncBucketKey, SyncBucketState)>,
846    ) -> BoxFuture<'static, StoreResult<()>> {
847        let store = self.clone();
848        Box::pin(async move {
849            let mut stored = store.state.lock().expect("in-memory store poisoned");
850            stored.sync_buckets.extend(states);
851            Ok(())
852        })
853    }
854
855    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
856        let store = self.clone();
857        Box::pin(async move {
858            store
859                .state
860                .lock()
861                .expect("in-memory store poisoned")
862                .sync_buckets
863                .remove(&key);
864            Ok(())
865        })
866    }
867
868    fn save_pending_sync_batch(
869        &self,
870        batch: PendingSyncBatch,
871    ) -> BoxFuture<'static, StoreResult<()>> {
872        let store = self.clone();
873        Box::pin(async move {
874            store
875                .state
876                .lock()
877                .expect("in-memory store poisoned")
878                .pending_sync_batches
879                .insert(batch.key, batch);
880            Ok(())
881        })
882    }
883
884    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
885        let store = self.clone();
886        Box::pin(async move {
887            let mut batches = store
888                .state
889                .lock()
890                .expect("in-memory store poisoned")
891                .pending_sync_batches
892                .values()
893                .cloned()
894                .collect::<Vec<_>>();
895            batches.sort_by_key(|batch| sync_bucket_sort_key(batch.key));
896            Ok(batches)
897        })
898    }
899
900    fn discard_pending_sync_batch(
901        &self,
902        key: SyncBucketKey,
903    ) -> BoxFuture<'static, StoreResult<()>> {
904        let store = self.clone();
905        Box::pin(async move {
906            store
907                .state
908                .lock()
909                .expect("in-memory store poisoned")
910                .pending_sync_batches
911                .remove(&key);
912            Ok(())
913        })
914    }
915
916    fn commit_pending_sync_batch(
917        &self,
918        key: SyncBucketKey,
919        state: SyncBucketState,
920    ) -> BoxFuture<'static, StoreResult<()>> {
921        let store = self.clone();
922        Box::pin(async move {
923            let mut stored = store.state.lock().expect("in-memory store poisoned");
924            stored.sync_buckets.insert(key, state);
925            stored.pending_sync_batches.remove(&key);
926            Ok(())
927        })
928    }
929
930    fn commit_pending_sync_batch_with_events(
931        &self,
932        key: SyncBucketKey,
933        state: SyncBucketState,
934        events: Vec<ClientEvent>,
935    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
936        let store = self.clone();
937        Box::pin(async move {
938            let mut stored = store.state.lock().expect("in-memory store poisoned");
939            let deliveries = append_memory_client_events(&mut stored, events);
940            stored.sync_buckets.insert(key, state);
941            stored.pending_sync_batches.remove(&key);
942            Ok(deliveries)
943        })
944    }
945
946    fn append_client_events(
947        &self,
948        events: Vec<ClientEvent>,
949    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
950        let store = self.clone();
951        Box::pin(async move {
952            let mut stored = store.state.lock().expect("in-memory store poisoned");
953            Ok(append_memory_client_events(&mut stored, events))
954        })
955    }
956
957    fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
958        let store = self.clone();
959        Box::pin(async move {
960            Ok(store
961                .state
962                .lock()
963                .expect("in-memory store poisoned")
964                .client_event_outbox
965                .clone())
966        })
967    }
968
969    fn acknowledge_client_event(&self, delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
970        let store = self.clone();
971        Box::pin(async move {
972            store
973                .state
974                .lock()
975                .expect("in-memory store poisoned")
976                .client_event_outbox
977                .retain(|delivery| delivery.delivery_id != Some(delivery_id));
978            Ok(())
979        })
980    }
981
982    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
983        let store = self.clone();
984        Box::pin(async move {
985            let state = store.state.lock().expect("in-memory store poisoned");
986            let limit = request.limit.unwrap_or(50).max(1) as usize;
987            let (dialogs, next_cursor) = match request.order {
988                DialogsOrder::RecentActivity => {
989                    let start = parse_cursor(request.cursor.as_deref())?;
990                    let dialogs = state
991                        .dialogs
992                        .iter()
993                        .skip(start)
994                        .take(limit)
995                        .cloned()
996                        .collect::<Vec<_>>();
997                    let next = start + dialogs.len();
998                    (
999                        dialogs,
1000                        (next < state.dialogs.len()).then(|| next.to_string()),
1001                    )
1002                }
1003                DialogsOrder::StableChatId => {
1004                    let after = parse_stable_dialog_cursor(request.cursor.as_deref())?;
1005                    let mut candidates = state
1006                        .dialogs
1007                        .iter()
1008                        .filter(|dialog| dialog.chat_id.get() > after)
1009                        .cloned()
1010                        .collect::<Vec<_>>();
1011                    candidates.sort_by_key(|dialog| dialog.chat_id.get());
1012                    let has_more = candidates.len() > limit;
1013                    candidates.truncate(limit);
1014                    let next = has_more
1015                        .then(|| {
1016                            candidates
1017                                .last()
1018                                .map(|dialog| dialog.chat_id.get().to_string())
1019                        })
1020                        .flatten();
1021                    (candidates, next)
1022                }
1023            };
1024            let dialogs = dialogs
1025                .into_iter()
1026                .map(|dialog| {
1027                    let chat_id = dialog.chat_id;
1028                    dialog_with_synced_through(
1029                        dialog,
1030                        max_message_id_from_memory(&state.messages, chat_id),
1031                    )
1032                })
1033                .collect::<Vec<_>>();
1034            let users = all_users_from_memory(&state.users);
1035            Ok(DialogsPage {
1036                dialogs,
1037                users,
1038                next_cursor,
1039            })
1040        })
1041    }
1042
1043    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
1044        let store = self.clone();
1045        Box::pin(async move {
1046            let state = store.state.lock().expect("in-memory store poisoned");
1047            Ok(state
1048                .dialogs
1049                .iter()
1050                .find(|dialog| dialog.chat_id == chat_id)
1051                .cloned()
1052                .map(|dialog| {
1053                    dialog_with_synced_through(
1054                        dialog,
1055                        max_message_id_from_memory(&state.messages, chat_id),
1056                    )
1057                }))
1058        })
1059    }
1060
1061    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
1062        let store = self.clone();
1063        Box::pin(async move {
1064            store.upsert_dialog(dialog);
1065            Ok(())
1066        })
1067    }
1068
1069    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
1070        let store = self.clone();
1071        Box::pin(async move {
1072            let mut state = store.state.lock().expect("in-memory store poisoned");
1073            state.dialogs.retain(|dialog| dialog.chat_id != chat_id);
1074            if let Some(messages) = state.messages.remove(&chat_id.get()) {
1075                state.message_tombstones.extend(
1076                    messages
1077                        .into_iter()
1078                        .map(|message| (chat_id.get(), message.message_id.get())),
1079                );
1080            }
1081            state.participants.remove(&chat_id.get());
1082            state.participant_snapshots.remove(&chat_id.get());
1083            state
1084                .reactions
1085                .retain(|_, reaction| reaction.chat_id != chat_id);
1086            state
1087                .reaction_snapshots
1088                .retain(|(stored_chat_id, _)| *stored_chat_id != chat_id.get());
1089            state.read_states.remove(&chat_id.get());
1090            state.chat_tombstones.insert(chat_id.get());
1091            Ok(())
1092        })
1093    }
1094
1095    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1096        let store = self.clone();
1097        Box::pin(async move {
1098            let mut ids = store
1099                .state
1100                .lock()
1101                .expect("in-memory store poisoned")
1102                .chat_tombstones
1103                .iter()
1104                .copied()
1105                .map(InlineId::new)
1106                .collect::<Vec<_>>();
1107            ids.sort_by_key(|id| id.get());
1108            Ok(ids)
1109        })
1110    }
1111
1112    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
1113        let store = self.clone();
1114        Box::pin(async move {
1115            let mut state = store.state.lock().expect("in-memory store poisoned");
1116            for user in users {
1117                state.users.insert(user.user_id.get(), user);
1118            }
1119            Ok(())
1120        })
1121    }
1122
1123    fn user(&self, user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
1124        let store = self.clone();
1125        Box::pin(async move {
1126            Ok(store
1127                .state
1128                .lock()
1129                .expect("in-memory store poisoned")
1130                .users
1131                .get(&user_id.get())
1132                .cloned())
1133        })
1134    }
1135
1136    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
1137        let store = self.clone();
1138        Box::pin(async move {
1139            store
1140                .state
1141                .lock()
1142                .expect("in-memory store poisoned")
1143                .spaces
1144                .insert(space.space_id.get(), space);
1145            Ok(())
1146        })
1147    }
1148
1149    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
1150        let store = self.clone();
1151        Box::pin(async move {
1152            Ok(store
1153                .state
1154                .lock()
1155                .expect("in-memory store poisoned")
1156                .spaces
1157                .get(&space_id.get())
1158                .cloned())
1159        })
1160    }
1161
1162    fn record_space_member(
1163        &self,
1164        member: SpaceMemberRecord,
1165    ) -> BoxFuture<'static, StoreResult<()>> {
1166        let store = self.clone();
1167        Box::pin(async move {
1168            store
1169                .state
1170                .lock()
1171                .expect("in-memory store poisoned")
1172                .space_members
1173                .insert((member.space_id.get(), member.user_id.get()), member);
1174            Ok(())
1175        })
1176    }
1177
1178    fn record_space_members(
1179        &self,
1180        space_id: InlineId,
1181        members: Vec<SpaceMemberRecord>,
1182    ) -> BoxFuture<'static, StoreResult<()>> {
1183        let store = self.clone();
1184        Box::pin(async move {
1185            if members.iter().any(|member| member.space_id != space_id) {
1186                return Err(StoreError::new(
1187                    ClientErrorCategory::InvalidInput,
1188                    "space member snapshot contains a different space id",
1189                ));
1190            }
1191            let mut state = store.state.lock().expect("in-memory store poisoned");
1192            state
1193                .space_members
1194                .retain(|(stored_space_id, _), _| *stored_space_id != space_id.get());
1195            for member in members {
1196                state
1197                    .space_members
1198                    .insert((space_id.get(), member.user_id.get()), member);
1199            }
1200            Ok(())
1201        })
1202    }
1203
1204    fn remove_space_member(
1205        &self,
1206        space_id: InlineId,
1207        user_id: InlineId,
1208    ) -> BoxFuture<'static, StoreResult<()>> {
1209        let store = self.clone();
1210        Box::pin(async move {
1211            store
1212                .state
1213                .lock()
1214                .expect("in-memory store poisoned")
1215                .space_members
1216                .remove(&(space_id.get(), user_id.get()));
1217            Ok(())
1218        })
1219    }
1220
1221    fn space_members(
1222        &self,
1223        space_id: InlineId,
1224    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
1225        let store = self.clone();
1226        Box::pin(async move {
1227            let mut members = store
1228                .state
1229                .lock()
1230                .expect("in-memory store poisoned")
1231                .space_members
1232                .iter()
1233                .filter(|((stored_space_id, _), _)| *stored_space_id == space_id.get())
1234                .map(|(_, member)| member.clone())
1235                .collect::<Vec<_>>();
1236            members.sort_by_key(|member| member.user_id.get());
1237            Ok(members)
1238        })
1239    }
1240
1241    fn record_user_settings(
1242        &self,
1243        settings: UserSettingsRecord,
1244    ) -> BoxFuture<'static, StoreResult<()>> {
1245        let store = self.clone();
1246        Box::pin(async move {
1247            store
1248                .state
1249                .lock()
1250                .expect("in-memory store poisoned")
1251                .user_settings = Some(settings);
1252            Ok(())
1253        })
1254    }
1255
1256    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
1257        let store = self.clone();
1258        Box::pin(async move {
1259            Ok(store
1260                .state
1261                .lock()
1262                .expect("in-memory store poisoned")
1263                .user_settings
1264                .clone())
1265        })
1266    }
1267
1268    fn record_chat_participants(
1269        &self,
1270        chat_id: InlineId,
1271        participants: Vec<ChatParticipantRecord>,
1272    ) -> BoxFuture<'static, StoreResult<()>> {
1273        let store = self.clone();
1274        Box::pin(async move {
1275            let snapshot = participants
1276                .into_iter()
1277                .map(|participant| (participant.user_id.get(), participant))
1278                .collect();
1279            let mut state = store.state.lock().expect("in-memory store poisoned");
1280            state.participants.insert(chat_id.get(), snapshot);
1281            state.participant_snapshots.insert(chat_id.get());
1282            Ok(())
1283        })
1284    }
1285
1286    fn record_chat_participant(
1287        &self,
1288        chat_id: InlineId,
1289        participant: ChatParticipantRecord,
1290    ) -> BoxFuture<'static, StoreResult<()>> {
1291        let store = self.clone();
1292        Box::pin(async move {
1293            store
1294                .state
1295                .lock()
1296                .expect("in-memory store poisoned")
1297                .participants
1298                .entry(chat_id.get())
1299                .or_default()
1300                .insert(participant.user_id.get(), participant);
1301            Ok(())
1302        })
1303    }
1304
1305    fn remove_chat_participant(
1306        &self,
1307        chat_id: InlineId,
1308        user_id: InlineId,
1309    ) -> BoxFuture<'static, StoreResult<()>> {
1310        let store = self.clone();
1311        Box::pin(async move {
1312            if let Some(participants) = store
1313                .state
1314                .lock()
1315                .expect("in-memory store poisoned")
1316                .participants
1317                .get_mut(&chat_id.get())
1318            {
1319                participants.remove(&user_id.get());
1320            }
1321            Ok(())
1322        })
1323    }
1324
1325    fn chat_participants(
1326        &self,
1327        chat_id: InlineId,
1328    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
1329        let store = self.clone();
1330        Box::pin(async move {
1331            let mut participants = store
1332                .state
1333                .lock()
1334                .expect("in-memory store poisoned")
1335                .participants
1336                .get(&chat_id.get())
1337                .map(|participants| participants.values().cloned().collect::<Vec<_>>())
1338                .unwrap_or_default();
1339            sort_participants(&mut participants);
1340            Ok(participants)
1341        })
1342    }
1343
1344    fn chat_participants_complete(
1345        &self,
1346        chat_id: InlineId,
1347    ) -> BoxFuture<'static, StoreResult<bool>> {
1348        let store = self.clone();
1349        Box::pin(async move {
1350            Ok(store
1351                .state
1352                .lock()
1353                .expect("in-memory store poisoned")
1354                .participant_snapshots
1355                .contains(&chat_id.get()))
1356        })
1357    }
1358
1359    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
1360        let store = self.clone();
1361        Box::pin(async move {
1362            let state = store.state.lock().expect("in-memory store poisoned");
1363            let messages = state
1364                .messages
1365                .get(&request.chat_id.get())
1366                .cloned()
1367                .unwrap_or_default();
1368            if request.before_message_id.is_some() && request.after_message_id.is_some() {
1369                return Err(StoreError::new(
1370                    ClientErrorCategory::InvalidInput,
1371                    "history request cannot specify both before_message_id and after_message_id",
1372                ));
1373            }
1374            let limit = request.limit.unwrap_or(50).max(1) as usize;
1375            let (messages, has_more) = select_history_window(
1376                messages,
1377                limit,
1378                request.before_message_id,
1379                request.after_message_id,
1380            );
1381            let users = users_for_messages_from_memory(&state.users, &messages);
1382            Ok(HistoryPage {
1383                messages,
1384                users,
1385                has_more,
1386                next_cursor: None,
1387            })
1388        })
1389    }
1390
1391    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
1392        let store = self.clone();
1393        Box::pin(async move {
1394            let mut state = store.state.lock().expect("in-memory store poisoned");
1395            state
1396                .message_tombstones
1397                .remove(&(message.chat_id.get(), message.message_id.get()));
1398            insert_message(&mut state.messages, message);
1399            Ok(())
1400        })
1401    }
1402
1403    fn record_message_deleted(
1404        &self,
1405        chat_id: InlineId,
1406        message_id: InlineId,
1407    ) -> BoxFuture<'static, StoreResult<()>> {
1408        let store = self.clone();
1409        Box::pin(async move {
1410            let mut state = store.state.lock().expect("in-memory store poisoned");
1411            if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1412                messages.retain(|message| message.message_id != message_id);
1413            }
1414            state.reactions.retain(|_, reaction| {
1415                reaction.chat_id != chat_id || reaction.message_id != message_id
1416            });
1417            state
1418                .reaction_snapshots
1419                .remove(&(chat_id.get(), message_id.get()));
1420            state
1421                .message_tombstones
1422                .insert((chat_id.get(), message_id.get()));
1423            Ok(())
1424        })
1425    }
1426
1427    fn message_deleted(
1428        &self,
1429        chat_id: InlineId,
1430        message_id: InlineId,
1431    ) -> BoxFuture<'static, StoreResult<bool>> {
1432        let store = self.clone();
1433        Box::pin(async move {
1434            Ok(store
1435                .state
1436                .lock()
1437                .expect("in-memory store poisoned")
1438                .message_tombstones
1439                .contains(&(chat_id.get(), message_id.get())))
1440        })
1441    }
1442
1443    fn deleted_message_ids(
1444        &self,
1445        chat_id: InlineId,
1446    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1447        let store = self.clone();
1448        Box::pin(async move {
1449            let mut ids = store
1450                .state
1451                .lock()
1452                .expect("in-memory store poisoned")
1453                .message_tombstones
1454                .iter()
1455                .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1456                .map(|(_, message_id)| InlineId::new(*message_id))
1457                .collect::<Vec<_>>();
1458            ids.sort_by_key(|id| id.get());
1459            Ok(ids)
1460        })
1461    }
1462
1463    fn clear_chat_messages(
1464        &self,
1465        chat_id: InlineId,
1466        before_date: Option<i64>,
1467    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1468        let store = self.clone();
1469        Box::pin(async move {
1470            let mut state = store.state.lock().expect("in-memory store poisoned");
1471            let mut removed = Vec::new();
1472            if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1473                messages.retain(|message| {
1474                    let should_remove = before_date.is_none_or(|date| message.timestamp < date);
1475                    if should_remove {
1476                        removed.push(message.message_id);
1477                    }
1478                    !should_remove
1479                });
1480            }
1481            let removed_ids = removed.iter().map(|id| id.get()).collect::<HashSet<_>>();
1482            state.reactions.retain(|_, reaction| {
1483                reaction.chat_id != chat_id || !removed_ids.contains(&reaction.message_id.get())
1484            });
1485            state
1486                .reaction_snapshots
1487                .retain(|(stored_chat_id, message_id)| {
1488                    *stored_chat_id != chat_id.get() || !removed_ids.contains(message_id)
1489                });
1490            state.message_tombstones.extend(
1491                removed_ids
1492                    .iter()
1493                    .map(|message_id| (chat_id.get(), *message_id)),
1494            );
1495            let last_message_id = state.messages.get(&chat_id.get()).and_then(|messages| {
1496                messages
1497                    .iter()
1498                    .map(|message| message.message_id)
1499                    .max_by_key(|id| id.get())
1500            });
1501            if let Some(dialog) = state
1502                .dialogs
1503                .iter_mut()
1504                .find(|dialog| dialog.chat_id == chat_id)
1505            {
1506                dialog.last_message_id = last_message_id;
1507            }
1508            removed.sort_by_key(|id| id.get());
1509            Ok(removed)
1510        })
1511    }
1512
1513    fn chat_ids_in_space(
1514        &self,
1515        space_id: InlineId,
1516    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1517        let store = self.clone();
1518        Box::pin(async move {
1519            let mut chat_ids = store
1520                .state
1521                .lock()
1522                .expect("in-memory store poisoned")
1523                .dialogs
1524                .iter()
1525                .filter(|dialog| dialog.space_id == Some(space_id))
1526                .map(|dialog| dialog.chat_id)
1527                .collect::<Vec<_>>();
1528            chat_ids.sort_by_key(|id| id.get());
1529            Ok(chat_ids)
1530        })
1531    }
1532
1533    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1534        let store = self.clone();
1535        Box::pin(async move {
1536            let key = reaction_key(&reaction);
1537            store
1538                .state
1539                .lock()
1540                .expect("in-memory store poisoned")
1541                .reactions
1542                .insert(key, reaction);
1543            Ok(())
1544        })
1545    }
1546
1547    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1548        let store = self.clone();
1549        Box::pin(async move {
1550            store
1551                .state
1552                .lock()
1553                .expect("in-memory store poisoned")
1554                .reactions
1555                .remove(&reaction_key(&reaction));
1556            Ok(())
1557        })
1558    }
1559
1560    fn replace_message_reactions(
1561        &self,
1562        chat_id: InlineId,
1563        message_id: InlineId,
1564        reactions: Vec<StoredReaction>,
1565    ) -> BoxFuture<'static, StoreResult<()>> {
1566        let store = self.clone();
1567        Box::pin(async move {
1568            let mut state = store.state.lock().expect("in-memory store poisoned");
1569            state.reactions.retain(|_, reaction| {
1570                reaction.chat_id != chat_id || reaction.message_id != message_id
1571            });
1572            for reaction in reactions {
1573                state.reactions.insert(reaction_key(&reaction), reaction);
1574            }
1575            state
1576                .reaction_snapshots
1577                .insert((chat_id.get(), message_id.get()));
1578            Ok(())
1579        })
1580    }
1581
1582    fn reactions(
1583        &self,
1584        chat_id: InlineId,
1585        message_id: InlineId,
1586    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1587        let store = self.clone();
1588        Box::pin(async move {
1589            let mut reactions = store
1590                .state
1591                .lock()
1592                .expect("in-memory store poisoned")
1593                .reactions
1594                .values()
1595                .filter(|reaction| reaction.chat_id == chat_id && reaction.message_id == message_id)
1596                .cloned()
1597                .collect::<Vec<_>>();
1598            sort_reactions(&mut reactions);
1599            Ok(reactions)
1600        })
1601    }
1602
1603    fn reactions_for_chat(
1604        &self,
1605        chat_id: InlineId,
1606    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1607        let store = self.clone();
1608        Box::pin(async move {
1609            let mut reactions = store
1610                .state
1611                .lock()
1612                .expect("in-memory store poisoned")
1613                .reactions
1614                .values()
1615                .filter(|reaction| reaction.chat_id == chat_id)
1616                .cloned()
1617                .collect::<Vec<_>>();
1618            reactions.sort_by(|left, right| {
1619                (
1620                    left.message_id.get(),
1621                    left.user_id.get(),
1622                    left.reaction.as_str(),
1623                )
1624                    .cmp(&(
1625                        right.message_id.get(),
1626                        right.user_id.get(),
1627                        right.reaction.as_str(),
1628                    ))
1629            });
1630            Ok(reactions)
1631        })
1632    }
1633
1634    fn reaction_snapshot_message_ids(
1635        &self,
1636        chat_id: InlineId,
1637    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1638        let store = self.clone();
1639        Box::pin(async move {
1640            let mut ids = store
1641                .state
1642                .lock()
1643                .expect("in-memory store poisoned")
1644                .reaction_snapshots
1645                .iter()
1646                .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1647                .map(|(_, message_id)| InlineId::new(*message_id))
1648                .collect::<Vec<_>>();
1649            ids.sort_by_key(|id| id.get());
1650            Ok(ids)
1651        })
1652    }
1653
1654    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
1655        let store = self.clone();
1656        Box::pin(async move {
1657            store
1658                .state
1659                .lock()
1660                .expect("in-memory store poisoned")
1661                .read_states
1662                .insert(state.chat_id.get(), state);
1663            Ok(())
1664        })
1665    }
1666
1667    fn read_state(
1668        &self,
1669        chat_id: InlineId,
1670    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
1671        let store = self.clone();
1672        Box::pin(async move {
1673            Ok(store
1674                .state
1675                .lock()
1676                .expect("in-memory store poisoned")
1677                .read_states
1678                .get(&chat_id.get())
1679                .copied())
1680        })
1681    }
1682
1683    fn record_transaction(
1684        &self,
1685        transaction: StoredTransaction,
1686    ) -> BoxFuture<'static, StoreResult<()>> {
1687        let store = self.clone();
1688        Box::pin(async move {
1689            let mut state = store.state.lock().expect("in-memory store poisoned");
1690            state.transactions.insert(
1691                transaction.identity.transaction_id.as_str().to_owned(),
1692                transaction,
1693            );
1694            Ok(())
1695        })
1696    }
1697
1698    fn transaction(
1699        &self,
1700        transaction_id: TransactionId,
1701    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
1702        let store = self.clone();
1703        Box::pin(async move {
1704            Ok(store
1705                .state
1706                .lock()
1707                .expect("in-memory store poisoned")
1708                .transactions
1709                .get(transaction_id.as_str())
1710                .cloned())
1711        })
1712    }
1713}
1714
1715/// SQLite-backed durable store.
1716#[derive(Clone)]
1717pub struct SqliteStore {
1718    connection: Arc<Mutex<Connection>>,
1719}
1720
1721impl fmt::Debug for SqliteStore {
1722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1723        f.debug_struct("SqliteStore")
1724            .field("connection", &"<sqlite>")
1725            .finish()
1726    }
1727}
1728
1729impl SqliteStore {
1730    /// Opens a SQLite store at `path`, creating parent directories and schema.
1731    pub fn open(path: impl AsRef<Path>) -> StoreResult<Self> {
1732        let path = path.as_ref();
1733        if let Some(parent) = path
1734            .parent()
1735            .filter(|parent| !parent.as_os_str().is_empty())
1736        {
1737            std::fs::create_dir_all(parent).map_err(|error| {
1738                StoreError::internal(format!("create store directory: {error}"))
1739            })?;
1740        }
1741        prepare_private_sqlite_file(path)?;
1742        let connection = Connection::open(path).map_err(sqlite_error)?;
1743        Self::from_connection(connection)
1744    }
1745
1746    /// Opens an in-memory SQLite store.
1747    pub fn open_in_memory() -> StoreResult<Self> {
1748        let connection = Connection::open_in_memory().map_err(sqlite_error)?;
1749        Self::from_connection(connection)
1750    }
1751
1752    fn from_connection(connection: Connection) -> StoreResult<Self> {
1753        migrate_sqlite(&connection)?;
1754        Ok(Self {
1755            connection: Arc::new(Mutex::new(connection)),
1756        })
1757    }
1758
1759    /// Inserts or replaces a dialog.
1760    pub fn upsert_dialog(&self, dialog: DialogRecord) -> StoreResult<()> {
1761        let connection = self.connection.lock().expect("sqlite store poisoned");
1762        upsert_sqlite_dialog(&connection, dialog)
1763    }
1764
1765    /// Inserts or replaces a user summary.
1766    pub fn upsert_user(&self, user: UserRecord) -> StoreResult<()> {
1767        let connection = self.connection.lock().expect("sqlite store poisoned");
1768        upsert_sqlite_user(&connection, user)
1769    }
1770
1771    /// Inserts a message into history.
1772    pub fn insert_message(&self, message: MessageRecord) -> StoreResult<()> {
1773        self.record_message_sync(message)
1774    }
1775
1776    fn save_session_sync(&self, session: StoredSession) -> StoreResult<()> {
1777        let auth_json = serde_json::to_string(&session.auth)
1778            .map_err(|error| StoreError::internal(format!("encode session auth: {error}")))?;
1779        let connection = self.connection.lock().expect("sqlite store poisoned");
1780        connection
1781            .execute(
1782                "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
1783                 VALUES (1, ?1, ?2, ?3)
1784                 ON CONFLICT(id) DO UPDATE SET
1785                   auth_json = excluded.auth_json,
1786                   account_namespace = excluded.account_namespace,
1787                   updated_at = excluded.updated_at",
1788                params![auth_json, session.account_namespace, now_seconds()],
1789            )
1790            .map_err(sqlite_error)?;
1791        Ok(())
1792    }
1793
1794    fn load_session_sync(&self) -> StoreResult<Option<StoredSession>> {
1795        let connection = self.connection.lock().expect("sqlite store poisoned");
1796        let row = connection
1797            .query_row(
1798                "SELECT auth_json, account_namespace FROM sessions WHERE id = 1",
1799                [],
1800                |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
1801            )
1802            .optional()
1803            .map_err(sqlite_error)?;
1804
1805        row.map(|(auth_json, account_namespace)| {
1806            let auth = serde_json::from_str::<AuthCredential>(&auth_json)
1807                .map_err(|error| StoreError::internal(format!("decode session auth: {error}")))?;
1808            Ok(StoredSession {
1809                auth,
1810                account_namespace,
1811            })
1812        })
1813        .transpose()
1814    }
1815
1816    fn clear_session_sync(&self) -> StoreResult<()> {
1817        let connection = self.connection.lock().expect("sqlite store poisoned");
1818        connection
1819            .execute("DELETE FROM sessions WHERE id = 1", [])
1820            .map_err(sqlite_error)?;
1821        Ok(())
1822    }
1823
1824    fn clear_account_data_sync(&self) -> StoreResult<()> {
1825        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1826        let transaction = connection.transaction().map_err(sqlite_error)?;
1827        for table in [
1828            "sync_state",
1829            "sync_buckets",
1830            "pending_sync_batches",
1831            "client_event_outbox",
1832            "dialogs",
1833            "chat_tombstones",
1834            "users",
1835            "spaces",
1836            "space_members",
1837            "user_settings",
1838            "chat_participants",
1839            "chat_participant_snapshots",
1840            "messages",
1841            "message_tombstones",
1842            "reactions",
1843            "reaction_snapshots",
1844            "read_states",
1845            "transactions",
1846        ] {
1847            transaction
1848                .execute(&format!("DELETE FROM {table}"), [])
1849                .map_err(sqlite_error)?;
1850        }
1851        transaction.commit().map_err(sqlite_error)?;
1852        Ok(())
1853    }
1854
1855    fn sync_state_sync(&self) -> StoreResult<SyncState> {
1856        let connection = self.connection.lock().expect("sqlite store poisoned");
1857        let last_sync_date = connection
1858            .query_row(
1859                "SELECT last_sync_date FROM sync_state WHERE id = 1",
1860                [],
1861                |row| row.get::<_, i64>(0),
1862            )
1863            .optional()
1864            .map_err(sqlite_error)?
1865            .unwrap_or_default();
1866        Ok(SyncState { last_sync_date })
1867    }
1868
1869    fn save_sync_state_sync(&self, state: SyncState) -> StoreResult<()> {
1870        let connection = self.connection.lock().expect("sqlite store poisoned");
1871        connection
1872            .execute(
1873                "INSERT INTO sync_state (id, last_sync_date, updated_at)
1874                 VALUES (1, ?1, ?2)
1875                 ON CONFLICT(id) DO UPDATE SET
1876                   last_sync_date = excluded.last_sync_date,
1877                   updated_at = excluded.updated_at",
1878                params![state.last_sync_date, now_seconds()],
1879            )
1880            .map_err(sqlite_error)?;
1881        Ok(())
1882    }
1883
1884    fn clear_sync_state_sync(&self) -> StoreResult<()> {
1885        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1886        let transaction = connection.transaction().map_err(sqlite_error)?;
1887        transaction
1888            .execute("DELETE FROM sync_state", [])
1889            .map_err(sqlite_error)?;
1890        transaction
1891            .execute("DELETE FROM sync_buckets", [])
1892            .map_err(sqlite_error)?;
1893        transaction.commit().map_err(sqlite_error)?;
1894        Ok(())
1895    }
1896
1897    fn sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<SyncBucketState> {
1898        let (kind, entity_id) = sync_bucket_key_parts(key);
1899        let connection = self.connection.lock().expect("sqlite store poisoned");
1900        let state = connection
1901            .query_row(
1902                "SELECT seq, date FROM sync_buckets
1903                 WHERE bucket_kind = ?1 AND entity_id = ?2",
1904                params![kind, entity_id],
1905                |row| {
1906                    Ok(SyncBucketState {
1907                        seq: row.get(0)?,
1908                        date: row.get(1)?,
1909                    })
1910                },
1911            )
1912            .optional()
1913            .map_err(sqlite_error)?
1914            .unwrap_or_default();
1915        Ok(state)
1916    }
1917
1918    fn save_sync_bucket_state_sync(
1919        &self,
1920        key: SyncBucketKey,
1921        state: SyncBucketState,
1922    ) -> StoreResult<()> {
1923        let (kind, entity_id) = sync_bucket_key_parts(key);
1924        let connection = self.connection.lock().expect("sqlite store poisoned");
1925        upsert_sync_bucket(&connection, kind, entity_id, state)
1926    }
1927
1928    fn save_sync_bucket_states_sync(
1929        &self,
1930        states: Vec<(SyncBucketKey, SyncBucketState)>,
1931    ) -> StoreResult<()> {
1932        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1933        let transaction = connection.transaction().map_err(sqlite_error)?;
1934        for (key, state) in states {
1935            let (kind, entity_id) = sync_bucket_key_parts(key);
1936            upsert_sync_bucket(&transaction, kind, entity_id, state)?;
1937        }
1938        transaction.commit().map_err(sqlite_error)?;
1939        Ok(())
1940    }
1941
1942    fn remove_sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<()> {
1943        let (kind, entity_id) = sync_bucket_key_parts(key);
1944        let connection = self.connection.lock().expect("sqlite store poisoned");
1945        connection
1946            .execute(
1947                "DELETE FROM sync_buckets WHERE bucket_kind = ?1 AND entity_id = ?2",
1948                params![kind, entity_id],
1949            )
1950            .map_err(sqlite_error)?;
1951        Ok(())
1952    }
1953
1954    fn save_pending_sync_batch_sync(&self, batch: PendingSyncBatch) -> StoreResult<()> {
1955        let (kind, entity_id) = sync_bucket_key_parts(batch.key);
1956        let connection = self.connection.lock().expect("sqlite store poisoned");
1957        connection
1958            .execute(
1959                "INSERT INTO pending_sync_batches (
1960                   bucket_kind, entity_id, seq, date, payload, updated_at
1961                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1962                 ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
1963                   seq = excluded.seq,
1964                   date = excluded.date,
1965                   payload = excluded.payload,
1966                   updated_at = excluded.updated_at",
1967                params![
1968                    kind,
1969                    entity_id,
1970                    batch.committed_state.seq,
1971                    batch.committed_state.date,
1972                    batch.payload,
1973                    now_seconds(),
1974                ],
1975            )
1976            .map_err(sqlite_error)?;
1977        Ok(())
1978    }
1979
1980    fn pending_sync_batches_sync(&self) -> StoreResult<Vec<PendingSyncBatch>> {
1981        let connection = self.connection.lock().expect("sqlite store poisoned");
1982        let mut statement = connection
1983            .prepare(
1984                "SELECT bucket_kind, entity_id, seq, date, payload
1985                 FROM pending_sync_batches
1986                 ORDER BY bucket_kind ASC, entity_id ASC",
1987            )
1988            .map_err(sqlite_error)?;
1989        statement
1990            .query_map([], |row| {
1991                let kind = row.get::<_, String>(0)?;
1992                let entity_id = row.get::<_, i64>(1)?;
1993                let key = sync_bucket_key_from_parts(&kind, entity_id).map_err(|error| {
1994                    rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
1995                })?;
1996                Ok(PendingSyncBatch {
1997                    key,
1998                    committed_state: SyncBucketState {
1999                        seq: row.get(2)?,
2000                        date: row.get(3)?,
2001                    },
2002                    payload: row.get(4)?,
2003                })
2004            })
2005            .map_err(sqlite_error)?
2006            .collect::<Result<Vec<_>, _>>()
2007            .map_err(sqlite_error)
2008    }
2009
2010    fn discard_pending_sync_batch_sync(&self, key: SyncBucketKey) -> StoreResult<()> {
2011        let (kind, entity_id) = sync_bucket_key_parts(key);
2012        let connection = self.connection.lock().expect("sqlite store poisoned");
2013        connection
2014            .execute(
2015                "DELETE FROM pending_sync_batches
2016                 WHERE bucket_kind = ?1 AND entity_id = ?2",
2017                params![kind, entity_id],
2018            )
2019            .map_err(sqlite_error)?;
2020        Ok(())
2021    }
2022
2023    fn commit_pending_sync_batch_sync(
2024        &self,
2025        key: SyncBucketKey,
2026        state: SyncBucketState,
2027        events: Vec<ClientEvent>,
2028    ) -> StoreResult<Vec<ClientEventDelivery>> {
2029        let (kind, entity_id) = sync_bucket_key_parts(key);
2030        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2031        let transaction = connection.transaction().map_err(sqlite_error)?;
2032        let deliveries = append_sqlite_client_events(&transaction, events)?;
2033        upsert_sync_bucket(&transaction, kind, entity_id, state)?;
2034        transaction
2035            .execute(
2036                "DELETE FROM pending_sync_batches
2037                 WHERE bucket_kind = ?1 AND entity_id = ?2",
2038                params![kind, entity_id],
2039            )
2040            .map_err(sqlite_error)?;
2041        transaction.commit().map_err(sqlite_error)?;
2042        Ok(deliveries)
2043    }
2044
2045    fn append_client_events_sync(
2046        &self,
2047        events: Vec<ClientEvent>,
2048    ) -> StoreResult<Vec<ClientEventDelivery>> {
2049        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2050        let transaction = connection.transaction().map_err(sqlite_error)?;
2051        let deliveries = append_sqlite_client_events(&transaction, events)?;
2052        transaction.commit().map_err(sqlite_error)?;
2053        Ok(deliveries)
2054    }
2055
2056    fn pending_client_events_sync(&self) -> StoreResult<Vec<ClientEventDelivery>> {
2057        let connection = self.connection.lock().expect("sqlite store poisoned");
2058        let mut statement = connection
2059            .prepare(
2060                "SELECT delivery_id, event_json
2061                 FROM client_event_outbox
2062                 ORDER BY delivery_id ASC",
2063            )
2064            .map_err(sqlite_error)?;
2065        statement
2066            .query_map([], |row| {
2067                let delivery_id = row.get::<_, i64>(0)?;
2068                let event_json = row.get::<_, String>(1)?;
2069                let event = serde_json::from_str(&event_json).map_err(|error| {
2070                    rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
2071                })?;
2072                Ok(ClientEventDelivery {
2073                    delivery_id: Some(u64::try_from(delivery_id).map_err(|error| {
2074                        rusqlite::Error::FromSqlConversionFailure(0, Type::Integer, Box::new(error))
2075                    })?),
2076                    event,
2077                })
2078            })
2079            .map_err(sqlite_error)?
2080            .collect::<Result<Vec<_>, _>>()
2081            .map_err(sqlite_error)
2082    }
2083
2084    fn acknowledge_client_event_sync(&self, delivery_id: u64) -> StoreResult<()> {
2085        let delivery_id = i64::try_from(delivery_id)
2086            .map_err(|_| StoreError::internal("client event delivery ID exceeded SQLite range"))?;
2087        let connection = self.connection.lock().expect("sqlite store poisoned");
2088        connection
2089            .execute(
2090                "DELETE FROM client_event_outbox WHERE delivery_id = ?1",
2091                params![delivery_id],
2092            )
2093            .map_err(sqlite_error)?;
2094        Ok(())
2095    }
2096
2097    fn dialogs_sync(&self, request: DialogsRequest) -> StoreResult<DialogsPage> {
2098        let limit = request.limit.unwrap_or(50).max(1) as usize;
2099        let connection = self.connection.lock().expect("sqlite store poisoned");
2100        let rows = match request.order {
2101            DialogsOrder::RecentActivity => {
2102                let start = parse_cursor(request.cursor.as_deref())?;
2103                let mut stmt = connection.prepare(
2104                    "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2105                            (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id) AS synced_through_message_id,
2106                            d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2107                            d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2108                            d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2109                     FROM dialogs d
2110                     ORDER BY COALESCE(last_message_id, 0) DESC, chat_id ASC
2111                     LIMIT ?1 OFFSET ?2",
2112                ).map_err(sqlite_error)?;
2113                stmt.query_map(
2114                    params![(limit + 1) as i64, start as i64],
2115                    sqlite_dialog_from_row,
2116                )
2117                .map_err(sqlite_error)?
2118                .collect::<Result<Vec<_>, _>>()
2119                .map_err(sqlite_error)?
2120            }
2121            DialogsOrder::StableChatId => {
2122                let after = parse_stable_dialog_cursor(request.cursor.as_deref())?;
2123                let mut stmt = connection.prepare(
2124                    "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2125                            (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id) AS synced_through_message_id,
2126                            d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2127                            d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2128                            d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2129                     FROM dialogs d
2130                     WHERE d.chat_id > ?1
2131                     ORDER BY d.chat_id ASC
2132                     LIMIT ?2",
2133                ).map_err(sqlite_error)?;
2134                stmt.query_map(params![after, (limit + 1) as i64], sqlite_dialog_from_row)
2135                    .map_err(sqlite_error)?
2136                    .collect::<Result<Vec<_>, _>>()
2137                    .map_err(sqlite_error)?
2138            }
2139        };
2140
2141        let has_more = rows.len() > limit;
2142        let dialogs = rows.into_iter().take(limit).collect::<Vec<_>>();
2143        let users = all_sqlite_users(&connection)?;
2144        let next_cursor = match request.order {
2145            DialogsOrder::RecentActivity => {
2146                let start = parse_cursor(request.cursor.as_deref())?;
2147                has_more.then(|| (start + dialogs.len()).to_string())
2148            }
2149            DialogsOrder::StableChatId => has_more
2150                .then(|| {
2151                    dialogs
2152                        .last()
2153                        .map(|dialog| dialog.chat_id.get().to_string())
2154                })
2155                .flatten(),
2156        };
2157        Ok(DialogsPage {
2158            next_cursor,
2159            dialogs,
2160            users,
2161        })
2162    }
2163
2164    fn dialog_sync(&self, chat_id: InlineId) -> StoreResult<Option<DialogRecord>> {
2165        let connection = self.connection.lock().expect("sqlite store poisoned");
2166        connection
2167            .query_row(
2168                "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2169                        (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id),
2170                        d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2171                        d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2172                        d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2173                 FROM dialogs d WHERE d.chat_id = ?1",
2174                params![chat_id.get()],
2175                sqlite_dialog_from_row,
2176            )
2177            .optional()
2178            .map_err(sqlite_error)
2179    }
2180
2181    fn remove_dialog_sync(&self, chat_id: InlineId) -> StoreResult<()> {
2182        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2183        let transaction = connection.transaction().map_err(sqlite_error)?;
2184        transaction
2185            .execute(
2186                "INSERT INTO chat_tombstones (chat_id, deleted_at)
2187                 VALUES (?1, ?2)
2188                 ON CONFLICT(chat_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2189                params![chat_id.get(), now_seconds()],
2190            )
2191            .map_err(sqlite_error)?;
2192        transaction
2193            .execute(
2194                "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2195                 SELECT chat_id, message_id, ?2 FROM messages WHERE chat_id = ?1
2196                 ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2197                params![chat_id.get(), now_seconds()],
2198            )
2199            .map_err(sqlite_error)?;
2200        for table in [
2201            "dialogs",
2202            "messages",
2203            "chat_participants",
2204            "chat_participant_snapshots",
2205            "reactions",
2206            "reaction_snapshots",
2207            "read_states",
2208        ] {
2209            transaction
2210                .execute(
2211                    &format!("DELETE FROM {table} WHERE chat_id = ?1"),
2212                    params![chat_id.get()],
2213                )
2214                .map_err(sqlite_error)?;
2215        }
2216        transaction.commit().map_err(sqlite_error)?;
2217        Ok(())
2218    }
2219
2220    fn deleted_chat_ids_sync(&self) -> StoreResult<Vec<InlineId>> {
2221        let connection = self.connection.lock().expect("sqlite store poisoned");
2222        let mut statement = connection
2223            .prepare("SELECT chat_id FROM chat_tombstones ORDER BY chat_id ASC")
2224            .map_err(sqlite_error)?;
2225        statement
2226            .query_map([], |row| row.get::<_, i64>(0).map(InlineId::new))
2227            .map_err(sqlite_error)?
2228            .collect::<Result<Vec<_>, _>>()
2229            .map_err(sqlite_error)
2230    }
2231
2232    fn history_sync(&self, request: HistoryRequest) -> StoreResult<HistoryPage> {
2233        let limit = request.limit.unwrap_or(50).max(1) as usize;
2234        let connection = self.connection.lock().expect("sqlite store poisoned");
2235        let rows = if request.before_message_id.is_some() && request.after_message_id.is_some() {
2236            return Err(StoreError::new(
2237                ClientErrorCategory::InvalidInput,
2238                "history request cannot specify both before_message_id and after_message_id",
2239            ));
2240        } else if let Some(before) = request.before_message_id {
2241            let mut stmt = connection
2242                .prepare(
2243                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2244                            content_json, reply_to_message_id, transaction_json
2245                     FROM messages
2246                     WHERE chat_id = ?1 AND message_id < ?2
2247                     ORDER BY message_id DESC
2248                     LIMIT ?3",
2249                )
2250                .map_err(sqlite_error)?;
2251            query_message_rows(
2252                &mut stmt,
2253                params![request.chat_id.get(), before.get(), (limit + 1) as i64],
2254            )?
2255        } else if let Some(after) = request.after_message_id {
2256            let mut stmt = connection
2257                .prepare(
2258                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2259                            content_json, reply_to_message_id, transaction_json
2260                     FROM messages
2261                     WHERE chat_id = ?1 AND message_id > ?2
2262                     ORDER BY message_id ASC
2263                     LIMIT ?3",
2264                )
2265                .map_err(sqlite_error)?;
2266            query_message_rows(
2267                &mut stmt,
2268                params![request.chat_id.get(), after.get(), (limit + 1) as i64],
2269            )?
2270        } else {
2271            let mut stmt = connection
2272                .prepare(
2273                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2274                            content_json, reply_to_message_id, transaction_json
2275                     FROM messages
2276                     WHERE chat_id = ?1
2277                     ORDER BY message_id DESC
2278                     LIMIT ?2",
2279                )
2280                .map_err(sqlite_error)?;
2281            query_message_rows(
2282                &mut stmt,
2283                params![request.chat_id.get(), (limit + 1) as i64],
2284            )?
2285        };
2286
2287        let messages = rows
2288            .into_iter()
2289            .map(raw_sqlite_message_to_record)
2290            .collect::<StoreResult<Vec<_>>>()?;
2291        let (messages, has_more) = select_history_window(
2292            messages,
2293            limit,
2294            request.before_message_id,
2295            request.after_message_id,
2296        );
2297        let users = sqlite_users_for_messages(&connection, &messages)?;
2298        Ok(HistoryPage {
2299            messages,
2300            users,
2301            has_more,
2302            next_cursor: None,
2303        })
2304    }
2305
2306    fn record_users_sync(&self, users: Vec<UserRecord>) -> StoreResult<()> {
2307        if users.is_empty() {
2308            return Ok(());
2309        }
2310        let connection = self.connection.lock().expect("sqlite store poisoned");
2311        for user in users {
2312            upsert_sqlite_user(&connection, user)?;
2313        }
2314        Ok(())
2315    }
2316
2317    fn user_sync(&self, user_id: InlineId) -> StoreResult<Option<UserRecord>> {
2318        let connection = self.connection.lock().expect("sqlite store poisoned");
2319        connection
2320            .query_row(
2321                "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
2322                 FROM users WHERE user_id = ?1",
2323                params![user_id.get()],
2324                sqlite_user_from_row,
2325            )
2326            .optional()
2327            .map_err(sqlite_error)
2328    }
2329
2330    fn record_space_sync(&self, space: SpaceRecord) -> StoreResult<()> {
2331        let payload = serde_json::to_string(&space)
2332            .map_err(|error| StoreError::internal(format!("encode space: {error}")))?;
2333        let connection = self.connection.lock().expect("sqlite store poisoned");
2334        connection
2335            .execute(
2336                "INSERT INTO spaces (space_id, record_json, updated_at)
2337                 VALUES (?1, ?2, ?3)
2338                 ON CONFLICT(space_id) DO UPDATE SET
2339                   record_json = excluded.record_json,
2340                   updated_at = excluded.updated_at",
2341                params![space.space_id.get(), payload, now_seconds()],
2342            )
2343            .map_err(sqlite_error)?;
2344        Ok(())
2345    }
2346
2347    fn space_sync(&self, space_id: InlineId) -> StoreResult<Option<SpaceRecord>> {
2348        let connection = self.connection.lock().expect("sqlite store poisoned");
2349        connection
2350            .query_row(
2351                "SELECT record_json FROM spaces WHERE space_id = ?1",
2352                params![space_id.get()],
2353                |row| row.get::<_, String>(0),
2354            )
2355            .optional()
2356            .map_err(sqlite_error)?
2357            .map(|payload| {
2358                serde_json::from_str(&payload)
2359                    .map_err(|error| StoreError::internal(format!("decode space: {error}")))
2360            })
2361            .transpose()
2362    }
2363
2364    fn record_space_member_sync(&self, member: SpaceMemberRecord) -> StoreResult<()> {
2365        let payload = serde_json::to_string(&member)
2366            .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
2367        let connection = self.connection.lock().expect("sqlite store poisoned");
2368        connection
2369            .execute(
2370                "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
2371                 VALUES (?1, ?2, ?3, ?4)
2372                 ON CONFLICT(space_id, user_id) DO UPDATE SET
2373                   record_json = excluded.record_json,
2374                   updated_at = excluded.updated_at",
2375                params![
2376                    member.space_id.get(),
2377                    member.user_id.get(),
2378                    payload,
2379                    now_seconds()
2380                ],
2381            )
2382            .map_err(sqlite_error)?;
2383        Ok(())
2384    }
2385
2386    fn record_space_members_sync(
2387        &self,
2388        space_id: InlineId,
2389        members: Vec<SpaceMemberRecord>,
2390    ) -> StoreResult<()> {
2391        if members.iter().any(|member| member.space_id != space_id) {
2392            return Err(StoreError::new(
2393                ClientErrorCategory::InvalidInput,
2394                "space member snapshot contains a different space id",
2395            ));
2396        }
2397        let mut encoded = Vec::with_capacity(members.len());
2398        for member in members {
2399            let payload = serde_json::to_string(&member)
2400                .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
2401            encoded.push((member, payload));
2402        }
2403        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2404        let transaction = connection.transaction().map_err(sqlite_error)?;
2405        transaction
2406            .execute(
2407                "DELETE FROM space_members WHERE space_id = ?1",
2408                params![space_id.get()],
2409            )
2410            .map_err(sqlite_error)?;
2411        for (member, payload) in encoded {
2412            transaction
2413                .execute(
2414                    "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
2415                     VALUES (?1, ?2, ?3, ?4)",
2416                    params![
2417                        member.space_id.get(),
2418                        member.user_id.get(),
2419                        payload,
2420                        now_seconds()
2421                    ],
2422                )
2423                .map_err(sqlite_error)?;
2424        }
2425        transaction.commit().map_err(sqlite_error)?;
2426        Ok(())
2427    }
2428
2429    fn remove_space_member_sync(&self, space_id: InlineId, user_id: InlineId) -> StoreResult<()> {
2430        let connection = self.connection.lock().expect("sqlite store poisoned");
2431        connection
2432            .execute(
2433                "DELETE FROM space_members WHERE space_id = ?1 AND user_id = ?2",
2434                params![space_id.get(), user_id.get()],
2435            )
2436            .map_err(sqlite_error)?;
2437        Ok(())
2438    }
2439
2440    fn space_members_sync(&self, space_id: InlineId) -> StoreResult<Vec<SpaceMemberRecord>> {
2441        let connection = self.connection.lock().expect("sqlite store poisoned");
2442        let mut statement = connection
2443            .prepare(
2444                "SELECT record_json FROM space_members
2445                 WHERE space_id = ?1 ORDER BY user_id ASC",
2446            )
2447            .map_err(sqlite_error)?;
2448        let payloads = statement
2449            .query_map(params![space_id.get()], |row| row.get::<_, String>(0))
2450            .map_err(sqlite_error)?
2451            .collect::<Result<Vec<_>, _>>()
2452            .map_err(sqlite_error)?;
2453        payloads
2454            .into_iter()
2455            .map(|payload| {
2456                serde_json::from_str(&payload)
2457                    .map_err(|error| StoreError::internal(format!("decode space member: {error}")))
2458            })
2459            .collect()
2460    }
2461
2462    fn record_user_settings_sync(&self, settings: UserSettingsRecord) -> StoreResult<()> {
2463        let payload = serde_json::to_string(&settings)
2464            .map_err(|error| StoreError::internal(format!("encode user settings: {error}")))?;
2465        let connection = self.connection.lock().expect("sqlite store poisoned");
2466        connection
2467            .execute(
2468                "INSERT INTO user_settings (id, settings_json, updated_at)
2469                 VALUES (1, ?1, ?2)
2470                 ON CONFLICT(id) DO UPDATE SET
2471                   settings_json = excluded.settings_json,
2472                   updated_at = excluded.updated_at",
2473                params![payload, now_seconds()],
2474            )
2475            .map_err(sqlite_error)?;
2476        Ok(())
2477    }
2478
2479    fn user_settings_sync(&self) -> StoreResult<Option<UserSettingsRecord>> {
2480        let connection = self.connection.lock().expect("sqlite store poisoned");
2481        connection
2482            .query_row(
2483                "SELECT settings_json FROM user_settings WHERE id = 1",
2484                [],
2485                |row| row.get::<_, String>(0),
2486            )
2487            .optional()
2488            .map_err(sqlite_error)?
2489            .map(|payload| {
2490                serde_json::from_str(&payload)
2491                    .map_err(|error| StoreError::internal(format!("decode user settings: {error}")))
2492            })
2493            .transpose()
2494    }
2495
2496    fn record_chat_participants_sync(
2497        &self,
2498        chat_id: InlineId,
2499        participants: Vec<ChatParticipantRecord>,
2500    ) -> StoreResult<()> {
2501        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2502        let transaction = connection.transaction().map_err(sqlite_error)?;
2503        transaction
2504            .execute(
2505                "DELETE FROM chat_participants WHERE chat_id = ?1",
2506                params![chat_id.get()],
2507            )
2508            .map_err(sqlite_error)?;
2509        for participant in participants {
2510            upsert_sqlite_participant(&transaction, chat_id, participant)?;
2511        }
2512        transaction
2513            .execute(
2514                "INSERT INTO chat_participant_snapshots (chat_id, updated_at)
2515                 VALUES (?1, ?2)
2516                 ON CONFLICT(chat_id) DO UPDATE SET updated_at = excluded.updated_at",
2517                params![chat_id.get(), now_seconds()],
2518            )
2519            .map_err(sqlite_error)?;
2520        transaction.commit().map_err(sqlite_error)?;
2521        Ok(())
2522    }
2523
2524    fn record_chat_participant_sync(
2525        &self,
2526        chat_id: InlineId,
2527        participant: ChatParticipantRecord,
2528    ) -> StoreResult<()> {
2529        let connection = self.connection.lock().expect("sqlite store poisoned");
2530        upsert_sqlite_participant(&connection, chat_id, participant)
2531    }
2532
2533    fn remove_chat_participant_sync(
2534        &self,
2535        chat_id: InlineId,
2536        user_id: InlineId,
2537    ) -> StoreResult<()> {
2538        let connection = self.connection.lock().expect("sqlite store poisoned");
2539        connection
2540            .execute(
2541                "DELETE FROM chat_participants WHERE chat_id = ?1 AND user_id = ?2",
2542                params![chat_id.get(), user_id.get()],
2543            )
2544            .map_err(sqlite_error)?;
2545        Ok(())
2546    }
2547
2548    fn chat_participants_sync(&self, chat_id: InlineId) -> StoreResult<Vec<ChatParticipantRecord>> {
2549        let connection = self.connection.lock().expect("sqlite store poisoned");
2550        let mut statement = connection
2551            .prepare(
2552                "SELECT user_id, date FROM chat_participants
2553                 WHERE chat_id = ?1 ORDER BY user_id ASC",
2554            )
2555            .map_err(sqlite_error)?;
2556        statement
2557            .query_map(params![chat_id.get()], |row| {
2558                Ok(ChatParticipantRecord {
2559                    user_id: InlineId::new(row.get(0)?),
2560                    date: row.get(1)?,
2561                })
2562            })
2563            .map_err(sqlite_error)?
2564            .collect::<Result<Vec<_>, _>>()
2565            .map_err(sqlite_error)
2566    }
2567
2568    fn chat_participants_complete_sync(&self, chat_id: InlineId) -> StoreResult<bool> {
2569        let connection = self.connection.lock().expect("sqlite store poisoned");
2570        connection
2571            .query_row(
2572                "SELECT 1 FROM chat_participant_snapshots WHERE chat_id = ?1",
2573                params![chat_id.get()],
2574                |_| Ok(()),
2575            )
2576            .optional()
2577            .map(|row| row.is_some())
2578            .map_err(sqlite_error)
2579    }
2580
2581    fn record_message_sync(&self, message: MessageRecord) -> StoreResult<()> {
2582        let content_json = serde_json::to_string(&message.content)
2583            .map_err(|error| StoreError::internal(format!("encode message content: {error}")))?;
2584        let transaction_json = message
2585            .transaction
2586            .as_ref()
2587            .map(serde_json::to_string)
2588            .transpose()
2589            .map_err(|error| {
2590                StoreError::internal(format!("encode transaction identity: {error}"))
2591            })?;
2592        let connection = self.connection.lock().expect("sqlite store poisoned");
2593        connection
2594            .execute(
2595                "INSERT INTO messages (
2596                   chat_id, message_id, sender_id, timestamp, is_outgoing,
2597                   content_json, reply_to_message_id, transaction_json
2598                 )
2599                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
2600                 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2601                   sender_id = excluded.sender_id,
2602                   timestamp = excluded.timestamp,
2603                   is_outgoing = excluded.is_outgoing,
2604                   content_json = excluded.content_json,
2605                   reply_to_message_id = excluded.reply_to_message_id,
2606                   transaction_json = excluded.transaction_json",
2607                params![
2608                    message.chat_id.get(),
2609                    message.message_id.get(),
2610                    message.sender_id.get(),
2611                    message.timestamp,
2612                    i64::from(message.is_outgoing),
2613                    content_json,
2614                    message.reply_to_message_id.map(InlineId::get),
2615                    transaction_json,
2616                ],
2617            )
2618            .map_err(sqlite_error)?;
2619        connection
2620            .execute(
2621                "DELETE FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2622                params![message.chat_id.get(), message.message_id.get()],
2623            )
2624            .map_err(sqlite_error)?;
2625        upsert_sqlite_dialog_last_message(&connection, message.chat_id, message.message_id)?;
2626        Ok(())
2627    }
2628
2629    fn record_message_deleted_sync(
2630        &self,
2631        chat_id: InlineId,
2632        message_id: InlineId,
2633    ) -> StoreResult<()> {
2634        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2635        let transaction = connection.transaction().map_err(sqlite_error)?;
2636        transaction
2637            .execute(
2638                "DELETE FROM messages WHERE chat_id = ?1 AND message_id = ?2",
2639                params![chat_id.get(), message_id.get()],
2640            )
2641            .map_err(sqlite_error)?;
2642        transaction
2643            .execute(
2644                "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2645                params![chat_id.get(), message_id.get()],
2646            )
2647            .map_err(sqlite_error)?;
2648        transaction
2649            .execute(
2650                "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2651                params![chat_id.get(), message_id.get()],
2652            )
2653            .map_err(sqlite_error)?;
2654        transaction
2655            .execute(
2656                "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2657                 VALUES (?1, ?2, ?3)
2658                 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2659                   deleted_at = excluded.deleted_at",
2660                params![chat_id.get(), message_id.get(), now_seconds()],
2661            )
2662            .map_err(sqlite_error)?;
2663        transaction.commit().map_err(sqlite_error)?;
2664        Ok(())
2665    }
2666
2667    fn message_deleted_sync(&self, chat_id: InlineId, message_id: InlineId) -> StoreResult<bool> {
2668        let connection = self.connection.lock().expect("sqlite store poisoned");
2669        connection
2670            .query_row(
2671                "SELECT 1 FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2672                params![chat_id.get(), message_id.get()],
2673                |_| Ok(()),
2674            )
2675            .optional()
2676            .map(|row| row.is_some())
2677            .map_err(sqlite_error)
2678    }
2679
2680    fn deleted_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2681        let connection = self.connection.lock().expect("sqlite store poisoned");
2682        let mut statement = connection
2683            .prepare(
2684                "SELECT message_id FROM message_tombstones
2685                 WHERE chat_id = ?1 ORDER BY message_id ASC",
2686            )
2687            .map_err(sqlite_error)?;
2688        statement
2689            .query_map(params![chat_id.get()], |row| {
2690                row.get::<_, i64>(0).map(InlineId::new)
2691            })
2692            .map_err(sqlite_error)?
2693            .collect::<Result<Vec<_>, _>>()
2694            .map_err(sqlite_error)
2695    }
2696
2697    fn clear_chat_messages_sync(
2698        &self,
2699        chat_id: InlineId,
2700        before_date: Option<i64>,
2701    ) -> StoreResult<Vec<InlineId>> {
2702        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2703        let transaction = connection.transaction().map_err(sqlite_error)?;
2704        let removed = {
2705            let (query, parameters): (&str, Vec<i64>) = match before_date {
2706                Some(date) => (
2707                    "SELECT message_id FROM messages
2708                     WHERE chat_id = ?1 AND timestamp < ?2 ORDER BY message_id ASC",
2709                    vec![chat_id.get(), date],
2710                ),
2711                None => (
2712                    "SELECT message_id FROM messages
2713                     WHERE chat_id = ?1 ORDER BY message_id ASC",
2714                    vec![chat_id.get()],
2715                ),
2716            };
2717            let mut statement = transaction.prepare(query).map_err(sqlite_error)?;
2718            statement
2719                .query_map(rusqlite::params_from_iter(parameters), |row| {
2720                    row.get::<_, i64>(0).map(InlineId::new)
2721                })
2722                .map_err(sqlite_error)?
2723                .collect::<Result<Vec<_>, _>>()
2724                .map_err(sqlite_error)?
2725        };
2726        for message_id in &removed {
2727            transaction
2728                .execute(
2729                    "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2730                    params![chat_id.get(), message_id.get()],
2731                )
2732                .map_err(sqlite_error)?;
2733            transaction
2734                .execute(
2735                    "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2736                    params![chat_id.get(), message_id.get()],
2737                )
2738                .map_err(sqlite_error)?;
2739            transaction
2740                .execute(
2741                    "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2742                     VALUES (?1, ?2, ?3)
2743                     ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2744                    params![chat_id.get(), message_id.get(), now_seconds()],
2745                )
2746                .map_err(sqlite_error)?;
2747        }
2748        match before_date {
2749            Some(date) => transaction
2750                .execute(
2751                    "DELETE FROM messages WHERE chat_id = ?1 AND timestamp < ?2",
2752                    params![chat_id.get(), date],
2753                )
2754                .map_err(sqlite_error)?,
2755            None => transaction
2756                .execute(
2757                    "DELETE FROM messages WHERE chat_id = ?1",
2758                    params![chat_id.get()],
2759                )
2760                .map_err(sqlite_error)?,
2761        };
2762        transaction
2763            .execute(
2764                "UPDATE dialogs SET
2765                   last_message_id = (SELECT MAX(message_id) FROM messages WHERE chat_id = ?1),
2766                   updated_at = ?2
2767                 WHERE chat_id = ?1",
2768                params![chat_id.get(), now_seconds()],
2769            )
2770            .map_err(sqlite_error)?;
2771        transaction.commit().map_err(sqlite_error)?;
2772        Ok(removed)
2773    }
2774
2775    fn chat_ids_in_space_sync(&self, space_id: InlineId) -> StoreResult<Vec<InlineId>> {
2776        let connection = self.connection.lock().expect("sqlite store poisoned");
2777        let mut statement = connection
2778            .prepare("SELECT chat_id FROM dialogs WHERE space_id = ?1 ORDER BY chat_id ASC")
2779            .map_err(sqlite_error)?;
2780        statement
2781            .query_map(params![space_id.get()], |row| {
2782                row.get::<_, i64>(0).map(InlineId::new)
2783            })
2784            .map_err(sqlite_error)?
2785            .collect::<Result<Vec<_>, _>>()
2786            .map_err(sqlite_error)
2787    }
2788
2789    fn record_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2790        let connection = self.connection.lock().expect("sqlite store poisoned");
2791        connection
2792            .execute(
2793                "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2794                 VALUES (?1, ?2, ?3, ?4, ?5)
2795                 ON CONFLICT(chat_id, message_id, user_id, reaction) DO UPDATE SET
2796                   updated_at = excluded.updated_at",
2797                params![
2798                    reaction.chat_id.get(),
2799                    reaction.message_id.get(),
2800                    reaction.user_id.get(),
2801                    reaction.reaction,
2802                    now_seconds(),
2803                ],
2804            )
2805            .map_err(sqlite_error)?;
2806        Ok(())
2807    }
2808
2809    fn remove_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2810        let connection = self.connection.lock().expect("sqlite store poisoned");
2811        connection
2812            .execute(
2813                "DELETE FROM reactions
2814                 WHERE chat_id = ?1 AND message_id = ?2 AND user_id = ?3 AND reaction = ?4",
2815                params![
2816                    reaction.chat_id.get(),
2817                    reaction.message_id.get(),
2818                    reaction.user_id.get(),
2819                    reaction.reaction,
2820                ],
2821            )
2822            .map_err(sqlite_error)?;
2823        Ok(())
2824    }
2825
2826    fn replace_message_reactions_sync(
2827        &self,
2828        chat_id: InlineId,
2829        message_id: InlineId,
2830        reactions: Vec<StoredReaction>,
2831    ) -> StoreResult<()> {
2832        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2833        let transaction = connection.transaction().map_err(sqlite_error)?;
2834        transaction
2835            .execute(
2836                "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2837                params![chat_id.get(), message_id.get()],
2838            )
2839            .map_err(sqlite_error)?;
2840        for reaction in reactions {
2841            transaction
2842                .execute(
2843                    "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2844                     VALUES (?1, ?2, ?3, ?4, ?5)",
2845                    params![
2846                        chat_id.get(),
2847                        message_id.get(),
2848                        reaction.user_id.get(),
2849                        reaction.reaction,
2850                        now_seconds(),
2851                    ],
2852                )
2853                .map_err(sqlite_error)?;
2854        }
2855        transaction
2856            .execute(
2857                "INSERT INTO reaction_snapshots (chat_id, message_id, updated_at)
2858                 VALUES (?1, ?2, ?3)
2859                 ON CONFLICT(chat_id, message_id) DO UPDATE SET updated_at = excluded.updated_at",
2860                params![chat_id.get(), message_id.get(), now_seconds()],
2861            )
2862            .map_err(sqlite_error)?;
2863        transaction.commit().map_err(sqlite_error)?;
2864        Ok(())
2865    }
2866
2867    fn reactions_sync(
2868        &self,
2869        chat_id: InlineId,
2870        message_id: InlineId,
2871    ) -> StoreResult<Vec<StoredReaction>> {
2872        let connection = self.connection.lock().expect("sqlite store poisoned");
2873        let mut statement = connection
2874            .prepare(
2875                "SELECT user_id, reaction FROM reactions
2876                 WHERE chat_id = ?1 AND message_id = ?2
2877                 ORDER BY user_id ASC, reaction ASC",
2878            )
2879            .map_err(sqlite_error)?;
2880        statement
2881            .query_map(params![chat_id.get(), message_id.get()], |row| {
2882                Ok(StoredReaction {
2883                    chat_id,
2884                    message_id,
2885                    user_id: InlineId::new(row.get(0)?),
2886                    reaction: row.get(1)?,
2887                })
2888            })
2889            .map_err(sqlite_error)?
2890            .collect::<Result<Vec<_>, _>>()
2891            .map_err(sqlite_error)
2892    }
2893
2894    fn reactions_for_chat_sync(&self, chat_id: InlineId) -> StoreResult<Vec<StoredReaction>> {
2895        let connection = self.connection.lock().expect("sqlite store poisoned");
2896        let mut statement = connection
2897            .prepare(
2898                "SELECT message_id, user_id, reaction FROM reactions
2899                 WHERE chat_id = ?1
2900                 ORDER BY message_id ASC, user_id ASC, reaction ASC",
2901            )
2902            .map_err(sqlite_error)?;
2903        statement
2904            .query_map(params![chat_id.get()], |row| {
2905                Ok(StoredReaction {
2906                    chat_id,
2907                    message_id: InlineId::new(row.get(0)?),
2908                    user_id: InlineId::new(row.get(1)?),
2909                    reaction: row.get(2)?,
2910                })
2911            })
2912            .map_err(sqlite_error)?
2913            .collect::<Result<Vec<_>, _>>()
2914            .map_err(sqlite_error)
2915    }
2916
2917    fn reaction_snapshot_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2918        let connection = self.connection.lock().expect("sqlite store poisoned");
2919        let mut statement = connection
2920            .prepare(
2921                "SELECT message_id FROM reaction_snapshots
2922                 WHERE chat_id = ?1 ORDER BY message_id ASC",
2923            )
2924            .map_err(sqlite_error)?;
2925        statement
2926            .query_map(params![chat_id.get()], |row| {
2927                row.get::<_, i64>(0).map(InlineId::new)
2928            })
2929            .map_err(sqlite_error)?
2930            .collect::<Result<Vec<_>, _>>()
2931            .map_err(sqlite_error)
2932    }
2933
2934    fn record_read_state_sync(&self, state: StoredReadState) -> StoreResult<()> {
2935        let connection = self.connection.lock().expect("sqlite store poisoned");
2936        connection
2937            .execute(
2938                "INSERT INTO read_states (
2939                   chat_id, read_max_id, unread_count, marked_unread, updated_at
2940                 ) VALUES (?1, ?2, ?3, ?4, ?5)
2941                 ON CONFLICT(chat_id) DO UPDATE SET
2942                   read_max_id = excluded.read_max_id,
2943                   unread_count = excluded.unread_count,
2944                   marked_unread = excluded.marked_unread,
2945                   updated_at = excluded.updated_at",
2946                params![
2947                    state.chat_id.get(),
2948                    state.read_max_id.map(InlineId::get),
2949                    state.unread_count.map(i64::from),
2950                    i64::from(state.marked_unread),
2951                    now_seconds(),
2952                ],
2953            )
2954            .map_err(sqlite_error)?;
2955        Ok(())
2956    }
2957
2958    fn read_state_sync(&self, chat_id: InlineId) -> StoreResult<Option<StoredReadState>> {
2959        let connection = self.connection.lock().expect("sqlite store poisoned");
2960        connection
2961            .query_row(
2962                "SELECT read_max_id, unread_count, marked_unread
2963                 FROM read_states WHERE chat_id = ?1",
2964                params![chat_id.get()],
2965                |row| {
2966                    Ok(StoredReadState {
2967                        chat_id,
2968                        read_max_id: row.get::<_, Option<i64>>(0)?.map(InlineId::new),
2969                        unread_count: row
2970                            .get::<_, Option<i64>>(1)?
2971                            .and_then(|value| u32::try_from(value).ok()),
2972                        marked_unread: row.get::<_, i64>(2)? != 0,
2973                    })
2974                },
2975            )
2976            .optional()
2977            .map_err(sqlite_error)
2978    }
2979
2980    fn record_transaction_sync(&self, transaction: StoredTransaction) -> StoreResult<()> {
2981        let identity_json = serde_json::to_string(&transaction.identity).map_err(|error| {
2982            StoreError::internal(format!("encode transaction identity: {error}"))
2983        })?;
2984        let state_json = serde_json::to_string(&transaction.state)
2985            .map_err(|error| StoreError::internal(format!("encode transaction state: {error}")))?;
2986        let failure_json = transaction
2987            .failure
2988            .as_ref()
2989            .map(serde_json::to_string)
2990            .transpose()
2991            .map_err(|error| {
2992                StoreError::internal(format!("encode transaction failure: {error}"))
2993            })?;
2994        let connection = self.connection.lock().expect("sqlite store poisoned");
2995        connection
2996            .execute(
2997                "INSERT INTO transactions (
2998                   transaction_id, identity_json, state_json, chat_id, message_id,
2999                   random_id, external_source, external_id, failure_json, created_at, updated_at
3000                 )
3001                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
3002                 ON CONFLICT(transaction_id) DO UPDATE SET
3003                   identity_json = excluded.identity_json,
3004                   state_json = excluded.state_json,
3005                   chat_id = excluded.chat_id,
3006                   message_id = excluded.message_id,
3007                   random_id = excluded.random_id,
3008                   external_source = excluded.external_source,
3009                   external_id = excluded.external_id,
3010                   failure_json = excluded.failure_json,
3011                   updated_at = excluded.updated_at",
3012                params![
3013                    transaction.identity.transaction_id.as_str(),
3014                    identity_json,
3015                    state_json,
3016                    transaction.chat_id.map(InlineId::get),
3017                    transaction.message_id.map(InlineId::get),
3018                    transaction.identity.random_id.get(),
3019                    transaction
3020                        .identity
3021                        .external_id
3022                        .as_ref()
3023                        .map(|external| external.source()),
3024                    transaction
3025                        .identity
3026                        .external_id
3027                        .as_ref()
3028                        .map(|external| external.id()),
3029                    failure_json,
3030                    transaction.created_at,
3031                    transaction.updated_at,
3032                ],
3033            )
3034            .map_err(sqlite_error)?;
3035        Ok(())
3036    }
3037
3038    fn transaction_sync(
3039        &self,
3040        transaction_id: TransactionId,
3041    ) -> StoreResult<Option<StoredTransaction>> {
3042        let connection = self.connection.lock().expect("sqlite store poisoned");
3043        let row = connection
3044            .query_row(
3045                "SELECT identity_json, state_json, chat_id, message_id,
3046                        failure_json, created_at, updated_at
3047                 FROM transactions
3048                 WHERE transaction_id = ?1",
3049                params![transaction_id.as_str()],
3050                |row| {
3051                    Ok(RawSqliteTransaction {
3052                        identity_json: row.get(0)?,
3053                        state_json: row.get(1)?,
3054                        chat_id: row.get(2)?,
3055                        message_id: row.get(3)?,
3056                        failure_json: row.get(4)?,
3057                        created_at: row.get(5)?,
3058                        updated_at: row.get(6)?,
3059                    })
3060                },
3061            )
3062            .optional()
3063            .map_err(sqlite_error)?;
3064
3065        row.map(raw_sqlite_transaction_to_record).transpose()
3066    }
3067}
3068
3069fn append_sqlite_client_events(
3070    connection: &Connection,
3071    events: Vec<ClientEvent>,
3072) -> StoreResult<Vec<ClientEventDelivery>> {
3073    events
3074        .into_iter()
3075        .map(|event| {
3076            if event.reliability() == EventReliability::BestEffort {
3077                return Ok(ClientEventDelivery::ephemeral(event));
3078            }
3079            let event_json = serde_json::to_string(&event)
3080                .map_err(|error| StoreError::internal(format!("encode client event: {error}")))?;
3081            connection
3082                .execute(
3083                    "INSERT INTO client_event_outbox (event_json, created_at) VALUES (?1, ?2)",
3084                    params![event_json, now_seconds()],
3085                )
3086                .map_err(sqlite_error)?;
3087            let delivery_id = u64::try_from(connection.last_insert_rowid())
3088                .map_err(|_| StoreError::internal("stored client event delivery ID was invalid"))?;
3089            Ok(ClientEventDelivery {
3090                delivery_id: Some(delivery_id),
3091                event,
3092            })
3093        })
3094        .collect()
3095}
3096
3097fn prepare_private_sqlite_file(path: &Path) -> StoreResult<()> {
3098    let mut options = OpenOptions::new();
3099    options.create(true).truncate(false).read(true).write(true);
3100    #[cfg(unix)]
3101    {
3102        use std::os::unix::fs::OpenOptionsExt;
3103        options.mode(0o600);
3104    }
3105    let _file = options
3106        .open(path)
3107        .map_err(|error| StoreError::internal(format!("prepare SQLite store file: {error}")))?;
3108    #[cfg(unix)]
3109    {
3110        use std::os::unix::fs::PermissionsExt;
3111        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(
3112            |error| StoreError::internal(format!("secure SQLite store permissions: {error}")),
3113        )?;
3114    }
3115    Ok(())
3116}
3117
3118impl ClientStore for SqliteStore {
3119    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
3120        let store = self.clone();
3121        Box::pin(async move { store.save_session_sync(session) })
3122    }
3123
3124    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
3125        let store = self.clone();
3126        Box::pin(async move { store.load_session_sync() })
3127    }
3128
3129    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
3130        let store = self.clone();
3131        Box::pin(async move { store.clear_session_sync() })
3132    }
3133
3134    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
3135        let store = self.clone();
3136        Box::pin(async move { store.clear_account_data_sync() })
3137    }
3138
3139    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
3140        let store = self.clone();
3141        Box::pin(async move { store.sync_state_sync() })
3142    }
3143
3144    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
3145        let store = self.clone();
3146        Box::pin(async move { store.save_sync_state_sync(state) })
3147    }
3148
3149    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
3150        let store = self.clone();
3151        Box::pin(async move { store.clear_sync_state_sync() })
3152    }
3153
3154    fn sync_bucket_state(
3155        &self,
3156        key: SyncBucketKey,
3157    ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
3158        let store = self.clone();
3159        Box::pin(async move { store.sync_bucket_state_sync(key) })
3160    }
3161
3162    fn save_sync_bucket_state(
3163        &self,
3164        key: SyncBucketKey,
3165        state: SyncBucketState,
3166    ) -> BoxFuture<'static, StoreResult<()>> {
3167        let store = self.clone();
3168        Box::pin(async move { store.save_sync_bucket_state_sync(key, state) })
3169    }
3170
3171    fn save_sync_bucket_states(
3172        &self,
3173        states: Vec<(SyncBucketKey, SyncBucketState)>,
3174    ) -> BoxFuture<'static, StoreResult<()>> {
3175        let store = self.clone();
3176        Box::pin(async move { store.save_sync_bucket_states_sync(states) })
3177    }
3178
3179    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
3180        let store = self.clone();
3181        Box::pin(async move { store.remove_sync_bucket_state_sync(key) })
3182    }
3183
3184    fn save_pending_sync_batch(
3185        &self,
3186        batch: PendingSyncBatch,
3187    ) -> BoxFuture<'static, StoreResult<()>> {
3188        let store = self.clone();
3189        Box::pin(async move { store.save_pending_sync_batch_sync(batch) })
3190    }
3191
3192    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
3193        let store = self.clone();
3194        Box::pin(async move { store.pending_sync_batches_sync() })
3195    }
3196
3197    fn discard_pending_sync_batch(
3198        &self,
3199        key: SyncBucketKey,
3200    ) -> BoxFuture<'static, StoreResult<()>> {
3201        let store = self.clone();
3202        Box::pin(async move { store.discard_pending_sync_batch_sync(key) })
3203    }
3204
3205    fn commit_pending_sync_batch(
3206        &self,
3207        key: SyncBucketKey,
3208        state: SyncBucketState,
3209    ) -> BoxFuture<'static, StoreResult<()>> {
3210        let store = self.clone();
3211        Box::pin(async move {
3212            store
3213                .commit_pending_sync_batch_sync(key, state, Vec::new())
3214                .map(|_| ())
3215        })
3216    }
3217
3218    fn commit_pending_sync_batch_with_events(
3219        &self,
3220        key: SyncBucketKey,
3221        state: SyncBucketState,
3222        events: Vec<ClientEvent>,
3223    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3224        let store = self.clone();
3225        Box::pin(async move { store.commit_pending_sync_batch_sync(key, state, events) })
3226    }
3227
3228    fn append_client_events(
3229        &self,
3230        events: Vec<ClientEvent>,
3231    ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3232        let store = self.clone();
3233        Box::pin(async move { store.append_client_events_sync(events) })
3234    }
3235
3236    fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3237        let store = self.clone();
3238        Box::pin(async move { store.pending_client_events_sync() })
3239    }
3240
3241    fn acknowledge_client_event(&self, delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
3242        let store = self.clone();
3243        Box::pin(async move { store.acknowledge_client_event_sync(delivery_id) })
3244    }
3245
3246    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
3247        let store = self.clone();
3248        Box::pin(async move { store.dialogs_sync(request) })
3249    }
3250
3251    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
3252        let store = self.clone();
3253        Box::pin(async move { store.dialog_sync(chat_id) })
3254    }
3255
3256    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
3257        let store = self.clone();
3258        Box::pin(async move { store.upsert_dialog(dialog) })
3259    }
3260
3261    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
3262        let store = self.clone();
3263        Box::pin(async move { store.remove_dialog_sync(chat_id) })
3264    }
3265
3266    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3267        let store = self.clone();
3268        Box::pin(async move { store.deleted_chat_ids_sync() })
3269    }
3270
3271    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
3272        let store = self.clone();
3273        Box::pin(async move { store.record_users_sync(users) })
3274    }
3275
3276    fn user(&self, user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
3277        let store = self.clone();
3278        Box::pin(async move { store.user_sync(user_id) })
3279    }
3280
3281    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
3282        let store = self.clone();
3283        Box::pin(async move { store.record_space_sync(space) })
3284    }
3285
3286    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
3287        let store = self.clone();
3288        Box::pin(async move { store.space_sync(space_id) })
3289    }
3290
3291    fn record_space_member(
3292        &self,
3293        member: SpaceMemberRecord,
3294    ) -> BoxFuture<'static, StoreResult<()>> {
3295        let store = self.clone();
3296        Box::pin(async move { store.record_space_member_sync(member) })
3297    }
3298
3299    fn record_space_members(
3300        &self,
3301        space_id: InlineId,
3302        members: Vec<SpaceMemberRecord>,
3303    ) -> BoxFuture<'static, StoreResult<()>> {
3304        let store = self.clone();
3305        Box::pin(async move { store.record_space_members_sync(space_id, members) })
3306    }
3307
3308    fn remove_space_member(
3309        &self,
3310        space_id: InlineId,
3311        user_id: InlineId,
3312    ) -> BoxFuture<'static, StoreResult<()>> {
3313        let store = self.clone();
3314        Box::pin(async move { store.remove_space_member_sync(space_id, user_id) })
3315    }
3316
3317    fn space_members(
3318        &self,
3319        space_id: InlineId,
3320    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
3321        let store = self.clone();
3322        Box::pin(async move { store.space_members_sync(space_id) })
3323    }
3324
3325    fn record_user_settings(
3326        &self,
3327        settings: UserSettingsRecord,
3328    ) -> BoxFuture<'static, StoreResult<()>> {
3329        let store = self.clone();
3330        Box::pin(async move { store.record_user_settings_sync(settings) })
3331    }
3332
3333    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
3334        let store = self.clone();
3335        Box::pin(async move { store.user_settings_sync() })
3336    }
3337
3338    fn record_chat_participants(
3339        &self,
3340        chat_id: InlineId,
3341        participants: Vec<ChatParticipantRecord>,
3342    ) -> BoxFuture<'static, StoreResult<()>> {
3343        let store = self.clone();
3344        Box::pin(async move { store.record_chat_participants_sync(chat_id, participants) })
3345    }
3346
3347    fn record_chat_participant(
3348        &self,
3349        chat_id: InlineId,
3350        participant: ChatParticipantRecord,
3351    ) -> BoxFuture<'static, StoreResult<()>> {
3352        let store = self.clone();
3353        Box::pin(async move { store.record_chat_participant_sync(chat_id, participant) })
3354    }
3355
3356    fn remove_chat_participant(
3357        &self,
3358        chat_id: InlineId,
3359        user_id: InlineId,
3360    ) -> BoxFuture<'static, StoreResult<()>> {
3361        let store = self.clone();
3362        Box::pin(async move { store.remove_chat_participant_sync(chat_id, user_id) })
3363    }
3364
3365    fn chat_participants(
3366        &self,
3367        chat_id: InlineId,
3368    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
3369        let store = self.clone();
3370        Box::pin(async move { store.chat_participants_sync(chat_id) })
3371    }
3372
3373    fn chat_participants_complete(
3374        &self,
3375        chat_id: InlineId,
3376    ) -> BoxFuture<'static, StoreResult<bool>> {
3377        let store = self.clone();
3378        Box::pin(async move { store.chat_participants_complete_sync(chat_id) })
3379    }
3380
3381    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
3382        let store = self.clone();
3383        Box::pin(async move { store.history_sync(request) })
3384    }
3385
3386    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
3387        let store = self.clone();
3388        Box::pin(async move { store.record_message_sync(message) })
3389    }
3390
3391    fn record_message_deleted(
3392        &self,
3393        chat_id: InlineId,
3394        message_id: InlineId,
3395    ) -> BoxFuture<'static, StoreResult<()>> {
3396        let store = self.clone();
3397        Box::pin(async move { store.record_message_deleted_sync(chat_id, message_id) })
3398    }
3399
3400    fn message_deleted(
3401        &self,
3402        chat_id: InlineId,
3403        message_id: InlineId,
3404    ) -> BoxFuture<'static, StoreResult<bool>> {
3405        let store = self.clone();
3406        Box::pin(async move { store.message_deleted_sync(chat_id, message_id) })
3407    }
3408
3409    fn deleted_message_ids(
3410        &self,
3411        chat_id: InlineId,
3412    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3413        let store = self.clone();
3414        Box::pin(async move { store.deleted_message_ids_sync(chat_id) })
3415    }
3416
3417    fn clear_chat_messages(
3418        &self,
3419        chat_id: InlineId,
3420        before_date: Option<i64>,
3421    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3422        let store = self.clone();
3423        Box::pin(async move { store.clear_chat_messages_sync(chat_id, before_date) })
3424    }
3425
3426    fn chat_ids_in_space(
3427        &self,
3428        space_id: InlineId,
3429    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3430        let store = self.clone();
3431        Box::pin(async move { store.chat_ids_in_space_sync(space_id) })
3432    }
3433
3434    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
3435        let store = self.clone();
3436        Box::pin(async move { store.record_reaction_sync(reaction) })
3437    }
3438
3439    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
3440        let store = self.clone();
3441        Box::pin(async move { store.remove_reaction_sync(reaction) })
3442    }
3443
3444    fn replace_message_reactions(
3445        &self,
3446        chat_id: InlineId,
3447        message_id: InlineId,
3448        reactions: Vec<StoredReaction>,
3449    ) -> BoxFuture<'static, StoreResult<()>> {
3450        let store = self.clone();
3451        Box::pin(
3452            async move { store.replace_message_reactions_sync(chat_id, message_id, reactions) },
3453        )
3454    }
3455
3456    fn reactions(
3457        &self,
3458        chat_id: InlineId,
3459        message_id: InlineId,
3460    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3461        let store = self.clone();
3462        Box::pin(async move { store.reactions_sync(chat_id, message_id) })
3463    }
3464
3465    fn reactions_for_chat(
3466        &self,
3467        chat_id: InlineId,
3468    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3469        let store = self.clone();
3470        Box::pin(async move { store.reactions_for_chat_sync(chat_id) })
3471    }
3472
3473    fn reaction_snapshot_message_ids(
3474        &self,
3475        chat_id: InlineId,
3476    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3477        let store = self.clone();
3478        Box::pin(async move { store.reaction_snapshot_message_ids_sync(chat_id) })
3479    }
3480
3481    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
3482        let store = self.clone();
3483        Box::pin(async move { store.record_read_state_sync(state) })
3484    }
3485
3486    fn read_state(
3487        &self,
3488        chat_id: InlineId,
3489    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
3490        let store = self.clone();
3491        Box::pin(async move { store.read_state_sync(chat_id) })
3492    }
3493
3494    fn record_transaction(
3495        &self,
3496        transaction: StoredTransaction,
3497    ) -> BoxFuture<'static, StoreResult<()>> {
3498        let store = self.clone();
3499        Box::pin(async move { store.record_transaction_sync(transaction) })
3500    }
3501
3502    fn transaction(
3503        &self,
3504        transaction_id: TransactionId,
3505    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
3506        let store = self.clone();
3507        Box::pin(async move { store.transaction_sync(transaction_id) })
3508    }
3509}
3510
3511pub(crate) fn parse_cursor(cursor: Option<&str>) -> StoreResult<usize> {
3512    match cursor {
3513        Some(cursor) if !cursor.trim().is_empty() => cursor.parse::<usize>().map_err(|_| {
3514            StoreError::new(
3515                ClientErrorCategory::InvalidInput,
3516                "invalid pagination cursor",
3517            )
3518        }),
3519        _ => Ok(0),
3520    }
3521}
3522
3523fn parse_stable_dialog_cursor(cursor: Option<&str>) -> StoreResult<i64> {
3524    match cursor {
3525        Some(cursor) if !cursor.trim().is_empty() => {
3526            let value = cursor.parse::<i64>().map_err(|_| {
3527                StoreError::new(
3528                    ClientErrorCategory::InvalidInput,
3529                    "invalid stable dialog pagination cursor",
3530                )
3531            })?;
3532            if value < 0 {
3533                return Err(StoreError::new(
3534                    ClientErrorCategory::InvalidInput,
3535                    "invalid stable dialog pagination cursor",
3536                ));
3537            }
3538            Ok(value)
3539        }
3540        _ => Ok(0),
3541    }
3542}
3543
3544fn migrate_sqlite(connection: &Connection) -> StoreResult<()> {
3545    connection
3546        .execute_batch(
3547            "
3548            PRAGMA foreign_keys = ON;
3549
3550            CREATE TABLE IF NOT EXISTS sessions (
3551                id INTEGER PRIMARY KEY CHECK (id = 1),
3552                auth_json TEXT NOT NULL,
3553                account_namespace TEXT,
3554                updated_at INTEGER NOT NULL
3555            );
3556
3557            CREATE TABLE IF NOT EXISTS sync_state (
3558                id INTEGER PRIMARY KEY CHECK (id = 1),
3559                last_sync_date INTEGER NOT NULL,
3560                updated_at INTEGER NOT NULL
3561            );
3562
3563            CREATE TABLE IF NOT EXISTS sync_buckets (
3564                bucket_kind TEXT NOT NULL,
3565                entity_id INTEGER NOT NULL,
3566                seq INTEGER NOT NULL,
3567                date INTEGER NOT NULL,
3568                updated_at INTEGER NOT NULL,
3569                PRIMARY KEY (bucket_kind, entity_id)
3570            );
3571
3572            CREATE TABLE IF NOT EXISTS pending_sync_batches (
3573                bucket_kind TEXT NOT NULL,
3574                entity_id INTEGER NOT NULL,
3575                seq INTEGER NOT NULL,
3576                date INTEGER NOT NULL,
3577                payload BLOB NOT NULL,
3578                updated_at INTEGER NOT NULL,
3579                PRIMARY KEY (bucket_kind, entity_id)
3580            );
3581
3582            CREATE TABLE IF NOT EXISTS client_event_outbox (
3583                delivery_id INTEGER PRIMARY KEY AUTOINCREMENT,
3584                event_json TEXT NOT NULL,
3585                created_at INTEGER NOT NULL
3586            );
3587
3588            CREATE TABLE IF NOT EXISTS dialogs (
3589                chat_id INTEGER PRIMARY KEY,
3590                peer_user_id INTEGER,
3591                title TEXT,
3592                emoji TEXT,
3593                last_message_id INTEGER,
3594                unread_count INTEGER,
3595                space_id INTEGER,
3596                is_public INTEGER,
3597                archived INTEGER,
3598                pinned INTEGER,
3599                open INTEGER,
3600                chat_list_hidden INTEGER,
3601                list_order TEXT,
3602                pinned_order TEXT,
3603                notification_mode TEXT,
3604                follow_mode TEXT,
3605                pinned_message_ids_json TEXT NOT NULL DEFAULT '[]',
3606                updated_at INTEGER NOT NULL
3607            );
3608
3609            CREATE TABLE IF NOT EXISTS chat_tombstones (
3610                chat_id INTEGER PRIMARY KEY,
3611                deleted_at INTEGER NOT NULL
3612            );
3613
3614            CREATE TABLE IF NOT EXISTS users (
3615                user_id INTEGER PRIMARY KEY,
3616                display_name TEXT,
3617                username TEXT,
3618                first_name TEXT,
3619                last_name TEXT,
3620                avatar_url TEXT,
3621                is_bot INTEGER,
3622                updated_at INTEGER NOT NULL
3623            );
3624
3625            CREATE TABLE IF NOT EXISTS spaces (
3626                space_id INTEGER PRIMARY KEY,
3627                record_json TEXT NOT NULL,
3628                updated_at INTEGER NOT NULL
3629            );
3630
3631            CREATE TABLE IF NOT EXISTS space_members (
3632                space_id INTEGER NOT NULL,
3633                user_id INTEGER NOT NULL,
3634                record_json TEXT NOT NULL,
3635                updated_at INTEGER NOT NULL,
3636                PRIMARY KEY (space_id, user_id)
3637            );
3638
3639            CREATE INDEX IF NOT EXISTS idx_space_members_space
3640                ON space_members (space_id, user_id);
3641
3642            CREATE TABLE IF NOT EXISTS user_settings (
3643                id INTEGER PRIMARY KEY CHECK (id = 1),
3644                settings_json TEXT NOT NULL,
3645                updated_at INTEGER NOT NULL
3646            );
3647
3648            CREATE TABLE IF NOT EXISTS chat_participants (
3649                chat_id INTEGER NOT NULL,
3650                user_id INTEGER NOT NULL,
3651                date INTEGER,
3652                updated_at INTEGER NOT NULL,
3653                PRIMARY KEY (chat_id, user_id)
3654            );
3655
3656            CREATE TABLE IF NOT EXISTS chat_participant_snapshots (
3657                chat_id INTEGER PRIMARY KEY,
3658                updated_at INTEGER NOT NULL
3659            );
3660
3661            CREATE TABLE IF NOT EXISTS messages (
3662                chat_id INTEGER NOT NULL,
3663                message_id INTEGER NOT NULL,
3664                sender_id INTEGER NOT NULL,
3665                timestamp INTEGER NOT NULL,
3666                is_outgoing INTEGER NOT NULL,
3667                content_json TEXT NOT NULL,
3668                reply_to_message_id INTEGER,
3669                transaction_json TEXT,
3670                PRIMARY KEY (chat_id, message_id)
3671            );
3672
3673            CREATE INDEX IF NOT EXISTS idx_messages_chat_message
3674                ON messages (chat_id, message_id DESC);
3675
3676            CREATE TABLE IF NOT EXISTS message_tombstones (
3677                chat_id INTEGER NOT NULL,
3678                message_id INTEGER NOT NULL,
3679                deleted_at INTEGER NOT NULL,
3680                PRIMARY KEY (chat_id, message_id)
3681            );
3682
3683            CREATE TABLE IF NOT EXISTS reactions (
3684                chat_id INTEGER NOT NULL,
3685                message_id INTEGER NOT NULL,
3686                user_id INTEGER NOT NULL,
3687                reaction TEXT NOT NULL,
3688                updated_at INTEGER NOT NULL,
3689                PRIMARY KEY (chat_id, message_id, user_id, reaction)
3690            );
3691
3692            CREATE INDEX IF NOT EXISTS idx_reactions_message
3693                ON reactions (chat_id, message_id);
3694
3695            CREATE TABLE IF NOT EXISTS reaction_snapshots (
3696                chat_id INTEGER NOT NULL,
3697                message_id INTEGER NOT NULL,
3698                updated_at INTEGER NOT NULL,
3699                PRIMARY KEY (chat_id, message_id)
3700            );
3701
3702            CREATE TABLE IF NOT EXISTS read_states (
3703                chat_id INTEGER PRIMARY KEY,
3704                read_max_id INTEGER,
3705                unread_count INTEGER,
3706                marked_unread INTEGER NOT NULL,
3707                updated_at INTEGER NOT NULL
3708            );
3709
3710            CREATE TABLE IF NOT EXISTS transactions (
3711                transaction_id TEXT PRIMARY KEY,
3712                identity_json TEXT NOT NULL,
3713                state_json TEXT NOT NULL,
3714                chat_id INTEGER,
3715                message_id INTEGER,
3716                random_id INTEGER NOT NULL,
3717                external_source TEXT,
3718                external_id TEXT,
3719                failure_json TEXT,
3720                created_at INTEGER NOT NULL,
3721                updated_at INTEGER NOT NULL
3722            );
3723
3724            CREATE INDEX IF NOT EXISTS idx_transactions_external
3725                ON transactions (external_source, external_id);
3726            CREATE INDEX IF NOT EXISTS idx_transactions_random
3727                ON transactions (random_id);
3728            ",
3729        )
3730        .map_err(sqlite_error)?;
3731    ensure_sqlite_column(connection, "sessions", "account_namespace", "TEXT")?;
3732    ensure_sqlite_column(connection, "dialogs", "peer_user_id", "INTEGER")?;
3733    ensure_sqlite_column(connection, "dialogs", "emoji", "TEXT")?;
3734    ensure_sqlite_column(connection, "dialogs", "space_id", "INTEGER")?;
3735    ensure_sqlite_column(connection, "dialogs", "is_public", "INTEGER")?;
3736    ensure_sqlite_column(connection, "dialogs", "archived", "INTEGER")?;
3737    ensure_sqlite_column(connection, "dialogs", "pinned", "INTEGER")?;
3738    ensure_sqlite_column(connection, "dialogs", "open", "INTEGER")?;
3739    ensure_sqlite_column(connection, "dialogs", "chat_list_hidden", "INTEGER")?;
3740    ensure_sqlite_column(connection, "dialogs", "list_order", "TEXT")?;
3741    ensure_sqlite_column(connection, "dialogs", "pinned_order", "TEXT")?;
3742    ensure_sqlite_column(connection, "dialogs", "notification_mode", "TEXT")?;
3743    ensure_sqlite_column(connection, "dialogs", "follow_mode", "TEXT")?;
3744    ensure_sqlite_column(
3745        connection,
3746        "dialogs",
3747        "pinned_message_ids_json",
3748        "TEXT NOT NULL DEFAULT '[]'",
3749    )?;
3750    Ok(())
3751}
3752
3753fn ensure_sqlite_column(
3754    connection: &Connection,
3755    table: &str,
3756    column: &str,
3757    definition: &str,
3758) -> StoreResult<()> {
3759    let pragma = format!("PRAGMA table_info({table})");
3760    let mut stmt = connection.prepare(&pragma).map_err(sqlite_error)?;
3761    let columns = stmt
3762        .query_map([], |row| row.get::<_, String>(1))
3763        .map_err(sqlite_error)?
3764        .collect::<Result<Vec<_>, _>>()
3765        .map_err(sqlite_error)?;
3766    if columns.iter().any(|name| name == column) {
3767        return Ok(());
3768    }
3769    connection
3770        .execute(
3771            &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
3772            [],
3773        )
3774        .map_err(sqlite_error)?;
3775    Ok(())
3776}
3777
3778fn sync_bucket_key_parts(key: SyncBucketKey) -> (&'static str, i64) {
3779    match key {
3780        SyncBucketKey::User => ("user", 0),
3781        SyncBucketKey::Space { space_id } => ("space", space_id.get()),
3782        SyncBucketKey::Chat {
3783            peer: SyncBucketPeer::User { user_id },
3784        } => ("chat_user", user_id.get()),
3785        SyncBucketKey::Chat {
3786            peer: SyncBucketPeer::Chat { chat_id },
3787        } => ("chat_chat", chat_id.get()),
3788    }
3789}
3790
3791fn sync_bucket_key_from_parts(kind: &str, entity_id: i64) -> StoreResult<SyncBucketKey> {
3792    match kind {
3793        "user" if entity_id == 0 => Ok(SyncBucketKey::User),
3794        "space" => Ok(SyncBucketKey::Space {
3795            space_id: InlineId::new(entity_id),
3796        }),
3797        "chat_user" => Ok(SyncBucketKey::Chat {
3798            peer: SyncBucketPeer::User {
3799                user_id: InlineId::new(entity_id),
3800            },
3801        }),
3802        "chat_chat" => Ok(SyncBucketKey::Chat {
3803            peer: SyncBucketPeer::Chat {
3804                chat_id: InlineId::new(entity_id),
3805            },
3806        }),
3807        _ => Err(StoreError::internal(format!(
3808            "invalid stored sync bucket {kind:?}/{entity_id}"
3809        ))),
3810    }
3811}
3812
3813fn sync_bucket_sort_key(key: SyncBucketKey) -> (u8, i64) {
3814    match key {
3815        SyncBucketKey::User => (0, 0),
3816        SyncBucketKey::Space { space_id } => (1, space_id.get()),
3817        SyncBucketKey::Chat {
3818            peer: SyncBucketPeer::User { user_id },
3819        } => (2, user_id.get()),
3820        SyncBucketKey::Chat {
3821            peer: SyncBucketPeer::Chat { chat_id },
3822        } => (3, chat_id.get()),
3823    }
3824}
3825
3826fn upsert_sync_bucket(
3827    connection: &Connection,
3828    kind: &str,
3829    entity_id: i64,
3830    state: SyncBucketState,
3831) -> StoreResult<()> {
3832    connection
3833        .execute(
3834            "INSERT INTO sync_buckets (bucket_kind, entity_id, seq, date, updated_at)
3835             VALUES (?1, ?2, ?3, ?4, ?5)
3836             ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
3837               seq = excluded.seq,
3838               date = excluded.date,
3839               updated_at = excluded.updated_at",
3840            params![kind, entity_id, state.seq, state.date, now_seconds()],
3841        )
3842        .map_err(sqlite_error)?;
3843    Ok(())
3844}
3845
3846fn upsert_sqlite_user(connection: &Connection, user: UserRecord) -> StoreResult<()> {
3847    connection
3848        .execute(
3849            "INSERT INTO users (
3850               user_id, display_name, username, first_name, last_name,
3851               avatar_url, is_bot, updated_at
3852             )
3853             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
3854             ON CONFLICT(user_id) DO UPDATE SET
3855               display_name = excluded.display_name,
3856               username = excluded.username,
3857               first_name = excluded.first_name,
3858               last_name = excluded.last_name,
3859               avatar_url = excluded.avatar_url,
3860               is_bot = excluded.is_bot,
3861               updated_at = excluded.updated_at",
3862            params![
3863                user.user_id.get(),
3864                user.display_name,
3865                user.username,
3866                user.first_name,
3867                user.last_name,
3868                user.avatar_url,
3869                user.is_bot.map(|is_bot| if is_bot { 1_i64 } else { 0_i64 }),
3870                now_seconds(),
3871            ],
3872        )
3873        .map_err(sqlite_error)?;
3874    Ok(())
3875}
3876
3877fn upsert_sqlite_participant(
3878    connection: &Connection,
3879    chat_id: InlineId,
3880    participant: ChatParticipantRecord,
3881) -> StoreResult<()> {
3882    connection
3883        .execute(
3884            "INSERT INTO chat_participants (chat_id, user_id, date, updated_at)
3885             VALUES (?1, ?2, ?3, ?4)
3886             ON CONFLICT(chat_id, user_id) DO UPDATE SET
3887               date = excluded.date,
3888               updated_at = excluded.updated_at",
3889            params![
3890                chat_id.get(),
3891                participant.user_id.get(),
3892                participant.date,
3893                now_seconds(),
3894            ],
3895        )
3896        .map_err(sqlite_error)?;
3897    Ok(())
3898}
3899
3900fn sqlite_dialog_from_row(row: &Row<'_>) -> rusqlite::Result<DialogRecord> {
3901    let pinned_json = row.get::<_, String>(17)?;
3902    let pinned_message_ids = serde_json::from_str(&pinned_json).map_err(|error| {
3903        rusqlite::Error::FromSqlConversionFailure(17, Type::Text, Box::new(error))
3904    })?;
3905    Ok(DialogRecord {
3906        chat_id: InlineId::new(row.get(0)?),
3907        peer_user_id: row.get::<_, Option<i64>>(1)?.map(InlineId::new),
3908        title: row.get(2)?,
3909        emoji: row.get(3)?,
3910        last_message_id: row.get::<_, Option<i64>>(4)?.map(InlineId::new),
3911        synced_through_message_id: row.get::<_, Option<i64>>(5)?.map(InlineId::new),
3912        unread_count: row
3913            .get::<_, Option<i64>>(6)?
3914            .and_then(|value| u32::try_from(value).ok()),
3915        space_id: row.get::<_, Option<i64>>(7)?.map(InlineId::new),
3916        is_public: row.get::<_, Option<bool>>(8)?,
3917        archived: row.get::<_, Option<bool>>(9)?,
3918        pinned: row.get::<_, Option<bool>>(10)?,
3919        open: row.get::<_, Option<bool>>(11)?,
3920        chat_list_hidden: row.get::<_, Option<bool>>(12)?,
3921        order: row.get(13)?,
3922        pinned_order: row.get(14)?,
3923        notification_mode: row
3924            .get::<_, Option<String>>(15)?
3925            .as_deref()
3926            .and_then(dialog_notification_mode_from_store),
3927        follow_mode: row
3928            .get::<_, Option<String>>(16)?
3929            .as_deref()
3930            .and_then(dialog_follow_mode_from_store),
3931        pinned_message_ids,
3932    })
3933}
3934
3935fn upsert_sqlite_dialog(connection: &Connection, dialog: DialogRecord) -> StoreResult<()> {
3936    let chat_id = dialog.chat_id;
3937    let pinned_message_ids_json = serde_json::to_string(&dialog.pinned_message_ids)
3938        .map_err(|error| StoreError::internal(format!("encode pinned message IDs: {error}")))?;
3939    connection
3940        .execute(
3941            "INSERT INTO dialogs (
3942               chat_id, peer_user_id, title, emoji, last_message_id, unread_count,
3943               space_id, is_public, archived, pinned, open, chat_list_hidden,
3944               list_order, pinned_order, notification_mode, follow_mode,
3945               pinned_message_ids_json, updated_at
3946             ) VALUES (
3947               ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
3948               ?14, ?15, ?16, ?17, ?18
3949             )
3950             ON CONFLICT(chat_id) DO UPDATE SET
3951               peer_user_id = excluded.peer_user_id,
3952               title = excluded.title,
3953               emoji = excluded.emoji,
3954               last_message_id = excluded.last_message_id,
3955               unread_count = excluded.unread_count,
3956               space_id = excluded.space_id,
3957               is_public = excluded.is_public,
3958               archived = excluded.archived,
3959               pinned = excluded.pinned,
3960               open = excluded.open,
3961               chat_list_hidden = excluded.chat_list_hidden,
3962               list_order = excluded.list_order,
3963               pinned_order = excluded.pinned_order,
3964               notification_mode = excluded.notification_mode,
3965               follow_mode = excluded.follow_mode,
3966               pinned_message_ids_json = excluded.pinned_message_ids_json,
3967               updated_at = excluded.updated_at",
3968            params![
3969                dialog.chat_id.get(),
3970                dialog.peer_user_id.map(InlineId::get),
3971                dialog.title,
3972                dialog.emoji,
3973                dialog.last_message_id.map(InlineId::get),
3974                dialog.unread_count.map(i64::from),
3975                dialog.space_id.map(InlineId::get),
3976                dialog.is_public.map(i64::from),
3977                dialog.archived.map(i64::from),
3978                dialog.pinned.map(i64::from),
3979                dialog.open.map(i64::from),
3980                dialog.chat_list_hidden.map(i64::from),
3981                dialog.order,
3982                dialog.pinned_order,
3983                dialog
3984                    .notification_mode
3985                    .map(dialog_notification_mode_for_store),
3986                dialog.follow_mode.map(dialog_follow_mode_for_store),
3987                pinned_message_ids_json,
3988                now_seconds(),
3989            ],
3990        )
3991        .map_err(sqlite_error)?;
3992    connection
3993        .execute(
3994            "DELETE FROM chat_tombstones WHERE chat_id = ?1",
3995            params![chat_id.get()],
3996        )
3997        .map_err(sqlite_error)?;
3998    Ok(())
3999}
4000
4001const fn dialog_notification_mode_for_store(mode: DialogNotificationMode) -> &'static str {
4002    match mode {
4003        DialogNotificationMode::All => "all",
4004        DialogNotificationMode::Mentions => "mentions",
4005        DialogNotificationMode::None => "none",
4006    }
4007}
4008
4009fn dialog_notification_mode_from_store(value: &str) -> Option<DialogNotificationMode> {
4010    match value {
4011        "all" => Some(DialogNotificationMode::All),
4012        "mentions" => Some(DialogNotificationMode::Mentions),
4013        "none" => Some(DialogNotificationMode::None),
4014        _ => None,
4015    }
4016}
4017
4018const fn dialog_follow_mode_for_store(mode: DialogFollowMode) -> &'static str {
4019    match mode {
4020        DialogFollowMode::Following => "following",
4021        DialogFollowMode::Unfollowed => "unfollowed",
4022    }
4023}
4024
4025fn dialog_follow_mode_from_store(value: &str) -> Option<DialogFollowMode> {
4026    match value {
4027        "following" => Some(DialogFollowMode::Following),
4028        "unfollowed" => Some(DialogFollowMode::Unfollowed),
4029        _ => None,
4030    }
4031}
4032
4033fn upsert_sqlite_dialog_last_message(
4034    connection: &Connection,
4035    chat_id: InlineId,
4036    message_id: InlineId,
4037) -> StoreResult<()> {
4038    let updated = connection
4039        .execute(
4040            "UPDATE dialogs
4041             SET last_message_id = ?1, updated_at = ?2
4042             WHERE chat_id = ?3",
4043            params![message_id.get(), now_seconds(), chat_id.get()],
4044        )
4045        .map_err(sqlite_error)?;
4046    if updated == 0 {
4047        connection
4048            .execute(
4049                "INSERT INTO dialogs (chat_id, title, last_message_id, unread_count, updated_at)
4050                 VALUES (?1, NULL, ?2, 0, ?3)",
4051                params![chat_id.get(), message_id.get(), now_seconds()],
4052            )
4053            .map_err(sqlite_error)?;
4054    }
4055    connection
4056        .execute(
4057            "DELETE FROM chat_tombstones WHERE chat_id = ?1",
4058            params![chat_id.get()],
4059        )
4060        .map_err(sqlite_error)?;
4061    Ok(())
4062}
4063
4064fn all_sqlite_users(connection: &Connection) -> StoreResult<Vec<UserRecord>> {
4065    let mut stmt = connection
4066        .prepare(
4067            "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
4068             FROM users
4069             ORDER BY user_id ASC",
4070        )
4071        .map_err(sqlite_error)?;
4072    query_user_rows(&mut stmt, [])
4073}
4074
4075fn sqlite_users_for_messages(
4076    connection: &Connection,
4077    messages: &[MessageRecord],
4078) -> StoreResult<Vec<UserRecord>> {
4079    let mut user_ids = messages
4080        .iter()
4081        .map(|message| message.sender_id.get())
4082        .collect::<Vec<_>>();
4083    user_ids.sort_unstable();
4084    user_ids.dedup();
4085
4086    let mut users = Vec::new();
4087    let mut stmt = connection
4088        .prepare(
4089            "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
4090             FROM users
4091             WHERE user_id = ?1",
4092        )
4093        .map_err(sqlite_error)?;
4094    for user_id in user_ids {
4095        if let Some(user) = stmt
4096            .query_row(params![user_id], sqlite_user_from_row)
4097            .optional()
4098            .map_err(sqlite_error)?
4099        {
4100            users.push(user);
4101        }
4102    }
4103    Ok(users)
4104}
4105
4106fn query_user_rows<P>(stmt: &mut rusqlite::Statement<'_>, params: P) -> StoreResult<Vec<UserRecord>>
4107where
4108    P: rusqlite::Params,
4109{
4110    stmt.query_map(params, sqlite_user_from_row)
4111        .map_err(sqlite_error)?
4112        .collect::<Result<Vec<_>, _>>()
4113        .map_err(sqlite_error)
4114}
4115
4116fn sqlite_user_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserRecord> {
4117    Ok(UserRecord {
4118        user_id: InlineId::new(row.get::<_, i64>(0)?),
4119        display_name: row.get::<_, Option<String>>(1)?,
4120        username: row.get::<_, Option<String>>(2)?,
4121        first_name: row.get::<_, Option<String>>(3)?,
4122        last_name: row.get::<_, Option<String>>(4)?,
4123        avatar_url: row.get::<_, Option<String>>(5)?,
4124        is_bot: row.get::<_, Option<i64>>(6)?.map(|value| value != 0),
4125    })
4126}
4127
4128#[derive(Debug)]
4129struct RawSqliteMessage {
4130    chat_id: i64,
4131    message_id: i64,
4132    sender_id: i64,
4133    timestamp: i64,
4134    is_outgoing: bool,
4135    content_json: String,
4136    reply_to_message_id: Option<i64>,
4137    transaction_json: Option<String>,
4138}
4139
4140fn query_message_rows<P>(
4141    stmt: &mut rusqlite::Statement<'_>,
4142    params: P,
4143) -> StoreResult<Vec<RawSqliteMessage>>
4144where
4145    P: rusqlite::Params,
4146{
4147    stmt.query_map(params, |row| {
4148        Ok(RawSqliteMessage {
4149            chat_id: row.get(0)?,
4150            message_id: row.get(1)?,
4151            sender_id: row.get(2)?,
4152            timestamp: row.get(3)?,
4153            is_outgoing: row.get::<_, i64>(4)? != 0,
4154            content_json: row.get(5)?,
4155            reply_to_message_id: row.get(6)?,
4156            transaction_json: row.get(7)?,
4157        })
4158    })
4159    .map_err(sqlite_error)?
4160    .collect::<Result<Vec<_>, _>>()
4161    .map_err(sqlite_error)
4162}
4163
4164fn raw_sqlite_message_to_record(raw: RawSqliteMessage) -> StoreResult<MessageRecord> {
4165    let content = serde_json::from_str::<MessageContent>(&raw.content_json)
4166        .map_err(|error| StoreError::internal(format!("decode message content: {error}")))?;
4167    let transaction = raw
4168        .transaction_json
4169        .as_deref()
4170        .map(serde_json::from_str::<TransactionIdentity>)
4171        .transpose()
4172        .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
4173    Ok(MessageRecord {
4174        chat_id: InlineId::new(raw.chat_id),
4175        message_id: InlineId::new(raw.message_id),
4176        sender_id: InlineId::new(raw.sender_id),
4177        timestamp: raw.timestamp,
4178        is_outgoing: raw.is_outgoing,
4179        content,
4180        reply_to_message_id: raw.reply_to_message_id.map(InlineId::new),
4181        transaction,
4182    })
4183}
4184
4185#[derive(Debug)]
4186struct RawSqliteTransaction {
4187    identity_json: String,
4188    state_json: String,
4189    chat_id: Option<i64>,
4190    message_id: Option<i64>,
4191    failure_json: Option<String>,
4192    created_at: i64,
4193    updated_at: i64,
4194}
4195
4196fn raw_sqlite_transaction_to_record(raw: RawSqliteTransaction) -> StoreResult<StoredTransaction> {
4197    let identity = serde_json::from_str::<TransactionIdentity>(&raw.identity_json)
4198        .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
4199    let state = serde_json::from_str::<TransactionState>(&raw.state_json)
4200        .map_err(|error| StoreError::internal(format!("decode transaction state: {error}")))?;
4201    let failure = raw
4202        .failure_json
4203        .as_deref()
4204        .map(serde_json::from_str::<ClientFailure>)
4205        .transpose()
4206        .map_err(|error| StoreError::internal(format!("decode transaction failure: {error}")))?;
4207    Ok(StoredTransaction {
4208        identity,
4209        state,
4210        chat_id: raw.chat_id.map(InlineId::new),
4211        message_id: raw.message_id.map(InlineId::new),
4212        failure,
4213        created_at: raw.created_at,
4214        updated_at: raw.updated_at,
4215    })
4216}
4217
4218fn sqlite_error(error: rusqlite::Error) -> StoreError {
4219    StoreError::internal(format!("sqlite store error: {error}"))
4220}
4221
4222fn now_seconds() -> i64 {
4223    SystemTime::now()
4224        .duration_since(UNIX_EPOCH)
4225        .map(|duration| duration.as_secs() as i64)
4226        .unwrap_or_default()
4227}
4228
4229fn all_users_from_memory(users: &HashMap<i64, UserRecord>) -> Vec<UserRecord> {
4230    let mut users = users.values().cloned().collect::<Vec<_>>();
4231    users.sort_by_key(|user| user.user_id.get());
4232    users
4233}
4234
4235fn max_message_id_from_memory(
4236    messages: &HashMap<i64, Vec<MessageRecord>>,
4237    chat_id: InlineId,
4238) -> Option<InlineId> {
4239    messages
4240        .get(&chat_id.get())?
4241        .iter()
4242        .map(|message| message.message_id.get())
4243        .max()
4244        .map(InlineId::new)
4245}
4246
4247fn dialog_with_synced_through(
4248    mut dialog: DialogRecord,
4249    synced_through_message_id: Option<InlineId>,
4250) -> DialogRecord {
4251    dialog.synced_through_message_id = synced_through_message_id;
4252    dialog
4253}
4254
4255fn users_for_messages_from_memory(
4256    users: &HashMap<i64, UserRecord>,
4257    messages: &[MessageRecord],
4258) -> Vec<UserRecord> {
4259    let mut user_ids = messages
4260        .iter()
4261        .map(|message| message.sender_id.get())
4262        .collect::<Vec<_>>();
4263    user_ids.sort_unstable();
4264    user_ids.dedup();
4265    user_ids
4266        .into_iter()
4267        .filter_map(|user_id| users.get(&user_id).cloned())
4268        .collect()
4269}
4270
4271fn upsert_dialog(dialogs: &mut Vec<DialogRecord>, dialog: DialogRecord) {
4272    if let Some(existing) = dialogs
4273        .iter_mut()
4274        .find(|existing| existing.chat_id == dialog.chat_id)
4275    {
4276        *existing = dialog;
4277        return;
4278    }
4279    dialogs.push(dialog);
4280}
4281
4282fn insert_message(messages: &mut HashMap<i64, Vec<MessageRecord>>, message: MessageRecord) {
4283    let chat_id = message.chat_id.get();
4284    messages.entry(chat_id).or_default().push(message);
4285    if let Some(messages) = messages.get_mut(&chat_id) {
4286        messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
4287    }
4288}
4289
4290fn reaction_key(reaction: &StoredReaction) -> (i64, i64, i64, String) {
4291    (
4292        reaction.chat_id.get(),
4293        reaction.message_id.get(),
4294        reaction.user_id.get(),
4295        reaction.reaction.clone(),
4296    )
4297}
4298
4299fn sort_reactions(reactions: &mut [StoredReaction]) {
4300    reactions.sort_by(|left, right| {
4301        (left.user_id.get(), left.reaction.as_str())
4302            .cmp(&(right.user_id.get(), right.reaction.as_str()))
4303    });
4304}
4305
4306fn sort_participants(participants: &mut [ChatParticipantRecord]) {
4307    participants.sort_by_key(|participant| participant.user_id.get());
4308}
4309
4310/// Updates or inserts a dialog's last message pointer.
4311pub(crate) fn upsert_dialog_last_message(
4312    dialogs: &mut Vec<DialogRecord>,
4313    chat_id: InlineId,
4314    message_id: InlineId,
4315) {
4316    if let Some(dialog) = dialogs.iter_mut().find(|dialog| dialog.chat_id == chat_id) {
4317        dialog.last_message_id = Some(message_id);
4318        return;
4319    }
4320    dialogs.push(DialogRecord {
4321        chat_id,
4322        peer_user_id: None,
4323        title: None,
4324        last_message_id: Some(message_id),
4325        synced_through_message_id: None,
4326        unread_count: Some(0),
4327        ..DialogRecord::new(chat_id)
4328    });
4329}
4330
4331#[cfg(test)]
4332mod tests {
4333    use crate::{AuthToken, ExternalId, RandomId};
4334
4335    use super::*;
4336
4337    fn session() -> StoredSession {
4338        StoredSession {
4339            auth: AuthCredential::AccessToken {
4340                token: AuthToken::try_new("secret-token").unwrap(),
4341            },
4342            account_namespace: Some("team".to_owned()),
4343        }
4344    }
4345
4346    fn test_message(message_id: i64) -> MessageRecord {
4347        MessageRecord {
4348            chat_id: InlineId::new(7),
4349            message_id: InlineId::new(message_id),
4350            sender_id: InlineId::new(2),
4351            timestamp: message_id,
4352            is_outgoing: false,
4353            content: MessageContent::Text {
4354                text: format!("message {message_id}"),
4355            },
4356            reply_to_message_id: None,
4357            transaction: None,
4358        }
4359    }
4360
4361    #[tokio::test]
4362    async fn stored_session_debug_redacts_token() {
4363        let rendered = format!("{:?}", session());
4364
4365        assert!(rendered.contains("[redacted]"));
4366        assert!(rendered.contains("<redacted>"));
4367        assert!(!rendered.contains("secret-token"));
4368        assert!(!rendered.contains("team"));
4369    }
4370
4371    #[tokio::test]
4372    async fn in_memory_store_saves_and_clears_session() {
4373        let store = InMemoryStore::new();
4374        store.save_session(session()).await.unwrap();
4375
4376        assert!(store.load_session().await.unwrap().is_some());
4377
4378        store.clear_session().await.unwrap();
4379        assert!(store.load_session().await.unwrap().is_none());
4380    }
4381
4382    #[tokio::test]
4383    async fn in_memory_store_round_trips_sync_cursors() {
4384        let store = InMemoryStore::new();
4385        let user = SyncBucketKey::User;
4386        let direct = SyncBucketKey::Chat {
4387            peer: SyncBucketPeer::User {
4388                user_id: InlineId::new(2),
4389            },
4390        };
4391        let chat = SyncBucketKey::Chat {
4392            peer: SyncBucketPeer::Chat {
4393                chat_id: InlineId::new(7),
4394            },
4395        };
4396        let space = SyncBucketKey::Space {
4397            space_id: InlineId::new(9),
4398        };
4399
4400        assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
4401        assert_eq!(
4402            store.sync_bucket_state(user).await.unwrap(),
4403            SyncBucketState::default()
4404        );
4405
4406        store
4407            .save_sync_state(SyncState { last_sync_date: 40 })
4408            .await
4409            .unwrap();
4410        store
4411            .save_sync_bucket_state(user, SyncBucketState { seq: 3, date: 30 })
4412            .await
4413            .unwrap();
4414        store
4415            .save_sync_bucket_states(vec![
4416                (direct, SyncBucketState { seq: 4, date: 31 }),
4417                (chat, SyncBucketState { seq: 5, date: 32 }),
4418                (space, SyncBucketState { seq: 6, date: 33 }),
4419            ])
4420            .await
4421            .unwrap();
4422        store
4423            .save_sync_bucket_state(user, SyncBucketState { seq: 7, date: 41 })
4424            .await
4425            .unwrap();
4426
4427        assert_eq!(
4428            store.sync_state().await.unwrap(),
4429            SyncState { last_sync_date: 40 }
4430        );
4431        assert_eq!(
4432            store.sync_bucket_state(user).await.unwrap(),
4433            SyncBucketState { seq: 7, date: 41 }
4434        );
4435        assert_eq!(
4436            store.sync_bucket_state(direct).await.unwrap(),
4437            SyncBucketState { seq: 4, date: 31 }
4438        );
4439        assert_eq!(
4440            store.sync_bucket_state(chat).await.unwrap(),
4441            SyncBucketState { seq: 5, date: 32 }
4442        );
4443        assert_eq!(
4444            store.sync_bucket_state(space).await.unwrap(),
4445            SyncBucketState { seq: 6, date: 33 }
4446        );
4447
4448        store.remove_sync_bucket_state(direct).await.unwrap();
4449        assert_eq!(
4450            store.sync_bucket_state(direct).await.unwrap(),
4451            SyncBucketState::default()
4452        );
4453
4454        store.clear_sync_state().await.unwrap();
4455        assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
4456        assert_eq!(
4457            store.sync_bucket_state(user).await.unwrap(),
4458            SyncBucketState::default()
4459        );
4460        assert_eq!(
4461            store.sync_bucket_state(space).await.unwrap(),
4462            SyncBucketState::default()
4463        );
4464    }
4465
4466    #[tokio::test]
4467    async fn in_memory_store_lists_dialogs_and_history() {
4468        let store = InMemoryStore::new();
4469        store.upsert_dialog(DialogRecord {
4470            chat_id: InlineId::new(7),
4471            peer_user_id: None,
4472            title: Some("general".to_owned()),
4473            last_message_id: None,
4474            synced_through_message_id: None,
4475            unread_count: Some(0),
4476            ..DialogRecord::new(InlineId::new(7))
4477        });
4478        store
4479            .record_users(vec![UserRecord {
4480                user_id: InlineId::new(2),
4481                display_name: Some("Ada Lovelace".to_owned()),
4482                username: Some("ada".to_owned()),
4483                first_name: Some("Ada".to_owned()),
4484                last_name: Some("Lovelace".to_owned()),
4485                avatar_url: None,
4486                is_bot: Some(false),
4487            }])
4488            .await
4489            .unwrap();
4490        store
4491            .record_message(MessageRecord {
4492                chat_id: InlineId::new(7),
4493                message_id: InlineId::new(1),
4494                sender_id: InlineId::new(2),
4495                timestamp: 1,
4496                is_outgoing: false,
4497                content: MessageContent::Text {
4498                    text: "hello".to_owned(),
4499                },
4500                reply_to_message_id: None,
4501                transaction: None,
4502            })
4503            .await
4504            .unwrap();
4505
4506        let dialogs = store.dialogs(DialogsRequest::default()).await.unwrap();
4507        assert_eq!(dialogs.dialogs.len(), 1);
4508        assert_eq!(
4509            dialogs.dialogs[0].synced_through_message_id,
4510            Some(InlineId::new(1))
4511        );
4512        assert_eq!(dialogs.users.len(), 1);
4513        assert_eq!(
4514            dialogs.users[0].display_name.as_deref(),
4515            Some("Ada Lovelace")
4516        );
4517
4518        let history = store
4519            .history(HistoryRequest {
4520                chat_id: InlineId::new(7),
4521                limit: Some(10),
4522                before_message_id: None,
4523                after_message_id: None,
4524            })
4525            .await
4526            .unwrap();
4527        assert_eq!(history.messages.len(), 1);
4528        assert_eq!(history.users.len(), 1);
4529        assert_eq!(history.users[0].user_id, InlineId::new(2));
4530    }
4531
4532    #[tokio::test]
4533    async fn stable_dialog_cursor_does_not_duplicate_when_activity_order_changes() {
4534        let store = InMemoryStore::new();
4535        for (chat_id, last_message_id) in [(10, 100), (30, 300), (50, 500)] {
4536            store.upsert_dialog(DialogRecord {
4537                chat_id: InlineId::new(chat_id),
4538                last_message_id: Some(InlineId::new(last_message_id)),
4539                ..DialogRecord::new(InlineId::new(chat_id))
4540            });
4541        }
4542
4543        let first = store
4544            .dialogs(DialogsRequest {
4545                limit: Some(2),
4546                cursor: None,
4547                order: DialogsOrder::StableChatId,
4548            })
4549            .await
4550            .unwrap();
4551        assert_eq!(
4552            first
4553                .dialogs
4554                .iter()
4555                .map(|dialog| dialog.chat_id.get())
4556                .collect::<Vec<_>>(),
4557            vec![10, 30]
4558        );
4559        assert_eq!(first.next_cursor.as_deref(), Some("30"));
4560
4561        store.upsert_dialog(DialogRecord {
4562            chat_id: InlineId::new(50),
4563            last_message_id: Some(InlineId::new(1_000)),
4564            ..DialogRecord::new(InlineId::new(50))
4565        });
4566        store.upsert_dialog(DialogRecord::new(InlineId::new(20)));
4567        store.upsert_dialog(DialogRecord::new(InlineId::new(40)));
4568
4569        let second = store
4570            .dialogs(DialogsRequest {
4571                limit: Some(2),
4572                cursor: first.next_cursor,
4573                order: DialogsOrder::StableChatId,
4574            })
4575            .await
4576            .unwrap();
4577        assert_eq!(
4578            second
4579                .dialogs
4580                .iter()
4581                .map(|dialog| dialog.chat_id.get())
4582                .collect::<Vec<_>>(),
4583            vec![40, 50]
4584        );
4585        assert_eq!(second.next_cursor, None);
4586    }
4587
4588    #[tokio::test]
4589    async fn in_memory_store_records_transactions() {
4590        let store = InMemoryStore::new();
4591        let transaction = stored_transaction("txn-mem", TransactionState::Queued);
4592
4593        store.record_transaction(transaction.clone()).await.unwrap();
4594
4595        let loaded = store
4596            .transaction(transaction.identity.transaction_id.clone())
4597            .await
4598            .unwrap()
4599            .unwrap();
4600        assert_eq!(
4601            loaded.identity.transaction_id,
4602            transaction.identity.transaction_id
4603        );
4604        assert_eq!(loaded.state, TransactionState::Queued);
4605    }
4606
4607    #[tokio::test]
4608    async fn in_memory_store_persists_message_reaction_and_read_mutations() {
4609        let store = InMemoryStore::new();
4610        let message = test_message(10);
4611        let reaction = StoredReaction {
4612            chat_id: message.chat_id,
4613            message_id: message.message_id,
4614            user_id: InlineId::new(2),
4615            reaction: "👍".to_owned(),
4616        };
4617        let read_state = StoredReadState {
4618            chat_id: message.chat_id,
4619            read_max_id: Some(message.message_id),
4620            unread_count: Some(3),
4621            marked_unread: false,
4622        };
4623
4624        store.record_message(message.clone()).await.unwrap();
4625        store.record_reaction(reaction.clone()).await.unwrap();
4626        store.record_read_state(read_state).await.unwrap();
4627        assert_eq!(
4628            store
4629                .reactions(message.chat_id, message.message_id)
4630                .await
4631                .unwrap(),
4632            vec![reaction.clone()]
4633        );
4634        assert_eq!(
4635            store.read_state(message.chat_id).await.unwrap(),
4636            Some(read_state)
4637        );
4638
4639        store
4640            .record_message_deleted(message.chat_id, message.message_id)
4641            .await
4642            .unwrap();
4643        assert!(
4644            store
4645                .message_deleted(message.chat_id, message.message_id)
4646                .await
4647                .unwrap()
4648        );
4649        assert!(
4650            store
4651                .reactions(message.chat_id, message.message_id)
4652                .await
4653                .unwrap()
4654                .is_empty()
4655        );
4656
4657        store.record_message(message.clone()).await.unwrap();
4658        assert!(
4659            !store
4660                .message_deleted(message.chat_id, message.message_id)
4661                .await
4662                .unwrap()
4663        );
4664    }
4665
4666    #[tokio::test]
4667    async fn in_memory_store_fetches_history_after_checkpoint() {
4668        let store = InMemoryStore::new();
4669        for message_id in 1..=3 {
4670            store
4671                .record_message(test_message(message_id))
4672                .await
4673                .unwrap();
4674        }
4675
4676        let history = store
4677            .history(HistoryRequest {
4678                chat_id: InlineId::new(7),
4679                limit: Some(1),
4680                before_message_id: None,
4681                after_message_id: Some(InlineId::new(1)),
4682            })
4683            .await
4684            .unwrap();
4685
4686        assert_eq!(history.messages.len(), 1);
4687        assert_eq!(history.messages[0].message_id, InlineId::new(2));
4688        assert!(history.has_more);
4689    }
4690
4691    #[tokio::test]
4692    async fn in_memory_store_latest_history_keeps_newest_window() {
4693        let store = InMemoryStore::new();
4694        for message_id in 1..=4 {
4695            store
4696                .record_message(test_message(message_id))
4697                .await
4698                .unwrap();
4699        }
4700
4701        let history = store
4702            .history(HistoryRequest {
4703                chat_id: InlineId::new(7),
4704                limit: Some(2),
4705                before_message_id: None,
4706                after_message_id: None,
4707            })
4708            .await
4709            .unwrap();
4710
4711        assert!(history.has_more);
4712        assert_eq!(
4713            history
4714                .messages
4715                .iter()
4716                .map(|message| message.message_id)
4717                .collect::<Vec<_>>(),
4718            vec![InlineId::new(3), InlineId::new(4)]
4719        );
4720    }
4721
4722    #[tokio::test]
4723    async fn sqlite_store_persists_session_dialogs_and_history() {
4724        let path = sqlite_temp_path("persist");
4725        let store = SqliteStore::open(&path).unwrap();
4726
4727        store.save_session(session()).await.unwrap();
4728        store
4729            .upsert_dialog(DialogRecord {
4730                chat_id: InlineId::new(7),
4731                peer_user_id: Some(InlineId::new(2)),
4732                title: Some("general".to_owned()),
4733                emoji: Some("🚀".to_owned()),
4734                last_message_id: None,
4735                synced_through_message_id: None,
4736                unread_count: Some(3),
4737                space_id: Some(InlineId::new(5)),
4738                is_public: Some(true),
4739                archived: Some(false),
4740                pinned: Some(true),
4741                open: Some(true),
4742                chat_list_hidden: Some(false),
4743                order: Some("a0".to_owned()),
4744                pinned_order: Some("p0".to_owned()),
4745                notification_mode: Some(DialogNotificationMode::Mentions),
4746                follow_mode: Some(DialogFollowMode::Following),
4747                pinned_message_ids: vec![InlineId::new(99), InlineId::new(98)],
4748            })
4749            .unwrap();
4750        store
4751            .record_users(vec![UserRecord {
4752                user_id: InlineId::new(2),
4753                display_name: Some("Grace Hopper".to_owned()),
4754                username: Some("grace".to_owned()),
4755                first_name: Some("Grace".to_owned()),
4756                last_name: Some("Hopper".to_owned()),
4757                avatar_url: Some("https://cdn.inline.test/grace.jpg".to_owned()),
4758                is_bot: Some(false),
4759            }])
4760            .await
4761            .unwrap();
4762        store
4763            .record_message(MessageRecord {
4764                chat_id: InlineId::new(7),
4765                message_id: InlineId::new(10),
4766                sender_id: InlineId::new(2),
4767                timestamp: 100,
4768                is_outgoing: false,
4769                content: MessageContent::Text {
4770                    text: "persisted".to_owned(),
4771                },
4772                reply_to_message_id: None,
4773                transaction: None,
4774            })
4775            .await
4776            .unwrap();
4777        drop(store);
4778
4779        let reopened = SqliteStore::open(&path).unwrap();
4780        let session = reopened.load_session().await.unwrap().unwrap();
4781        assert_eq!(session.account_namespace.as_deref(), Some("team"));
4782        assert!(!format!("{reopened:?}").contains(path.to_string_lossy().as_ref()));
4783
4784        let dialogs = reopened.dialogs(DialogsRequest::default()).await.unwrap();
4785        assert_eq!(dialogs.dialogs.len(), 1);
4786        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
4787        assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
4788        assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("general"));
4789        assert_eq!(dialogs.dialogs[0].emoji.as_deref(), Some("🚀"));
4790        assert_eq!(dialogs.dialogs[0].last_message_id, Some(InlineId::new(10)));
4791        assert_eq!(dialogs.dialogs[0].space_id, Some(InlineId::new(5)));
4792        assert_eq!(dialogs.dialogs[0].is_public, Some(true));
4793        assert_eq!(dialogs.dialogs[0].archived, Some(false));
4794        assert_eq!(dialogs.dialogs[0].pinned, Some(true));
4795        assert_eq!(dialogs.dialogs[0].open, Some(true));
4796        assert_eq!(dialogs.dialogs[0].chat_list_hidden, Some(false));
4797        assert_eq!(dialogs.dialogs[0].order.as_deref(), Some("a0"));
4798        assert_eq!(dialogs.dialogs[0].pinned_order.as_deref(), Some("p0"));
4799        assert_eq!(
4800            dialogs.dialogs[0].notification_mode,
4801            Some(DialogNotificationMode::Mentions)
4802        );
4803        assert_eq!(
4804            dialogs.dialogs[0].follow_mode,
4805            Some(DialogFollowMode::Following)
4806        );
4807        assert_eq!(
4808            dialogs.dialogs[0].pinned_message_ids,
4809            vec![InlineId::new(99), InlineId::new(98)]
4810        );
4811        assert_eq!(
4812            dialogs.dialogs[0].synced_through_message_id,
4813            Some(InlineId::new(10))
4814        );
4815        assert_eq!(dialogs.users.len(), 1);
4816        assert_eq!(
4817            dialogs.users[0].display_name.as_deref(),
4818            Some("Grace Hopper")
4819        );
4820
4821        let history = reopened
4822            .history(HistoryRequest {
4823                chat_id: InlineId::new(7),
4824                limit: Some(10),
4825                before_message_id: None,
4826                after_message_id: None,
4827            })
4828            .await
4829            .unwrap();
4830        assert_eq!(history.messages.len(), 1);
4831        assert_eq!(history.messages[0].message_id, InlineId::new(10));
4832        assert!(matches!(
4833            history.messages[0].content,
4834            MessageContent::Text { ref text } if text == "persisted"
4835        ));
4836        assert_eq!(history.users.len(), 1);
4837        assert_eq!(
4838            history.users[0].avatar_url.as_deref(),
4839            Some("https://cdn.inline.test/grace.jpg")
4840        );
4841
4842        let _ = std::fs::remove_file(path);
4843    }
4844
4845    #[cfg(unix)]
4846    #[tokio::test]
4847    async fn sqlite_store_file_permissions_are_private() {
4848        use std::os::unix::fs::PermissionsExt;
4849
4850        let path = sqlite_temp_path("permissions");
4851        let store = SqliteStore::open(&path).unwrap();
4852        store.save_session(session()).await.unwrap();
4853
4854        let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
4855        assert_eq!(mode, 0o600);
4856
4857        drop(store);
4858        let _ = std::fs::remove_file(path);
4859    }
4860
4861    async fn assert_clear_history_store(store: &dyn ClientStore) {
4862        let mut dialog = DialogRecord::new(InlineId::new(7));
4863        dialog.space_id = Some(InlineId::new(5));
4864        store.record_dialog(dialog).await.unwrap();
4865        for (message_id, timestamp) in [(10, 100), (11, 200)] {
4866            store
4867                .record_message(MessageRecord {
4868                    chat_id: InlineId::new(7),
4869                    message_id: InlineId::new(message_id),
4870                    sender_id: InlineId::new(2),
4871                    timestamp,
4872                    is_outgoing: false,
4873                    content: MessageContent::Text {
4874                        text: format!("message {message_id}"),
4875                    },
4876                    reply_to_message_id: None,
4877                    transaction: None,
4878                })
4879                .await
4880                .unwrap();
4881        }
4882        store
4883            .record_reaction(StoredReaction {
4884                chat_id: InlineId::new(7),
4885                message_id: InlineId::new(10),
4886                user_id: InlineId::new(2),
4887                reaction: "ok".to_owned(),
4888            })
4889            .await
4890            .unwrap();
4891
4892        assert_eq!(
4893            store.chat_ids_in_space(InlineId::new(5)).await.unwrap(),
4894            vec![InlineId::new(7)]
4895        );
4896        assert_eq!(
4897            store
4898                .clear_chat_messages(InlineId::new(7), Some(150))
4899                .await
4900                .unwrap(),
4901            vec![InlineId::new(10)]
4902        );
4903        assert!(
4904            store
4905                .message_deleted(InlineId::new(7), InlineId::new(10))
4906                .await
4907                .unwrap()
4908        );
4909        assert!(
4910            store
4911                .reactions(InlineId::new(7), InlineId::new(10))
4912                .await
4913                .unwrap()
4914                .is_empty()
4915        );
4916        let history = store
4917            .history(HistoryRequest {
4918                chat_id: InlineId::new(7),
4919                limit: Some(10),
4920                before_message_id: None,
4921                after_message_id: None,
4922            })
4923            .await
4924            .unwrap();
4925        assert_eq!(
4926            history
4927                .messages
4928                .iter()
4929                .map(|message| message.message_id)
4930                .collect::<Vec<_>>(),
4931            vec![InlineId::new(11)]
4932        );
4933        assert_eq!(
4934            store
4935                .dialog(InlineId::new(7))
4936                .await
4937                .unwrap()
4938                .unwrap()
4939                .last_message_id,
4940            Some(InlineId::new(11))
4941        );
4942    }
4943
4944    #[tokio::test]
4945    async fn stores_clear_history_with_tombstones_and_space_lookup() {
4946        let memory = InMemoryStore::new();
4947        assert_clear_history_store(&memory).await;
4948        let sqlite = SqliteStore::open_in_memory().unwrap();
4949        assert_clear_history_store(&sqlite).await;
4950    }
4951
4952    async fn assert_space_and_settings_store(store: &dyn ClientStore) {
4953        let space = SpaceRecord {
4954            space_id: InlineId::new(5),
4955            name: "Engineering".to_owned(),
4956            creator: true,
4957            date: 100,
4958            is_public: Some(false),
4959            grid_enabled: Some(true),
4960        };
4961        let member = SpaceMemberRecord {
4962            space_id: InlineId::new(5),
4963            user_id: InlineId::new(2),
4964            role: Some(crate::SpaceMemberRole::Admin),
4965            date: 101,
4966            can_access_public_chats: true,
4967        };
4968        let settings = UserSettingsRecord {
4969            notification_mode: Some(crate::NotificationMode::Mentions),
4970            silent: Some(true),
4971            zen_mode_requires_mention: Some(true),
4972            zen_mode_uses_default_rules: Some(false),
4973            zen_mode_custom_rules: Some("important".to_owned()),
4974            disable_dm_notifications: Some(false),
4975        };
4976
4977        store.record_space(space.clone()).await.unwrap();
4978        store.record_space_member(member.clone()).await.unwrap();
4979        store.record_user_settings(settings.clone()).await.unwrap();
4980
4981        assert_eq!(store.space(InlineId::new(5)).await.unwrap(), Some(space));
4982        assert_eq!(
4983            store.space_members(InlineId::new(5)).await.unwrap(),
4984            vec![member]
4985        );
4986        assert_eq!(store.user_settings().await.unwrap(), Some(settings));
4987        store
4988            .remove_space_member(InlineId::new(5), InlineId::new(2))
4989            .await
4990            .unwrap();
4991        assert!(
4992            store
4993                .space_members(InlineId::new(5))
4994                .await
4995                .unwrap()
4996                .is_empty()
4997        );
4998    }
4999
5000    #[tokio::test]
5001    async fn stores_persist_spaces_members_and_user_settings() {
5002        let memory = InMemoryStore::new();
5003        assert_space_and_settings_store(&memory).await;
5004        let sqlite = SqliteStore::open_in_memory().unwrap();
5005        assert_space_and_settings_store(&sqlite).await;
5006    }
5007
5008    #[tokio::test]
5009    async fn sqlite_store_persists_sync_cursors_across_reopen() {
5010        let path = sqlite_temp_path("sync-cursors");
5011        let store = SqliteStore::open(&path).unwrap();
5012        let user = SyncBucketKey::User;
5013        let direct = SyncBucketKey::Chat {
5014            peer: SyncBucketPeer::User {
5015                user_id: InlineId::new(2),
5016            },
5017        };
5018        let chat = SyncBucketKey::Chat {
5019            peer: SyncBucketPeer::Chat {
5020                chat_id: InlineId::new(7),
5021            },
5022        };
5023        let space = SyncBucketKey::Space {
5024            space_id: InlineId::new(9),
5025        };
5026
5027        store
5028            .save_sync_state(SyncState { last_sync_date: 80 })
5029            .await
5030            .unwrap();
5031        store
5032            .save_sync_bucket_states(vec![
5033                (user, SyncBucketState { seq: 10, date: 70 }),
5034                (direct, SyncBucketState { seq: 11, date: 71 }),
5035                (chat, SyncBucketState { seq: 12, date: 72 }),
5036                (space, SyncBucketState { seq: 13, date: 73 }),
5037            ])
5038            .await
5039            .unwrap();
5040        store
5041            .save_sync_bucket_state(direct, SyncBucketState { seq: 14, date: 74 })
5042            .await
5043            .unwrap();
5044        drop(store);
5045
5046        let reopened = SqliteStore::open(&path).unwrap();
5047        assert_eq!(
5048            reopened.sync_state().await.unwrap(),
5049            SyncState { last_sync_date: 80 }
5050        );
5051        assert_eq!(
5052            reopened.sync_bucket_state(user).await.unwrap(),
5053            SyncBucketState { seq: 10, date: 70 }
5054        );
5055        assert_eq!(
5056            reopened.sync_bucket_state(direct).await.unwrap(),
5057            SyncBucketState { seq: 14, date: 74 }
5058        );
5059        assert_eq!(
5060            reopened.sync_bucket_state(chat).await.unwrap(),
5061            SyncBucketState { seq: 12, date: 72 }
5062        );
5063        assert_eq!(
5064            reopened.sync_bucket_state(space).await.unwrap(),
5065            SyncBucketState { seq: 13, date: 73 }
5066        );
5067
5068        reopened.remove_sync_bucket_state(chat).await.unwrap();
5069        assert_eq!(
5070            reopened.sync_bucket_state(chat).await.unwrap(),
5071            SyncBucketState::default()
5072        );
5073        reopened.clear_sync_state().await.unwrap();
5074        assert_eq!(reopened.sync_state().await.unwrap(), SyncState::default());
5075        assert_eq!(
5076            reopened.sync_bucket_state(space).await.unwrap(),
5077            SyncBucketState::default()
5078        );
5079
5080        drop(reopened);
5081        let _ = std::fs::remove_file(path);
5082    }
5083
5084    #[tokio::test]
5085    async fn sqlite_store_upgrades_initial_client_schema_in_place() {
5086        let path = sqlite_temp_path("initial-client-upgrade");
5087        let connection = Connection::open(&path).unwrap();
5088        connection
5089            .execute_batch(
5090                "
5091                CREATE TABLE sessions (
5092                    id INTEGER PRIMARY KEY CHECK (id = 1),
5093                    auth_json TEXT NOT NULL,
5094                    account_namespace TEXT,
5095                    updated_at INTEGER NOT NULL
5096                );
5097                CREATE TABLE dialogs (
5098                    chat_id INTEGER PRIMARY KEY,
5099                    peer_user_id INTEGER,
5100                    title TEXT,
5101                    last_message_id INTEGER,
5102                    unread_count INTEGER,
5103                    updated_at INTEGER NOT NULL
5104                );
5105                CREATE TABLE users (
5106                    user_id INTEGER PRIMARY KEY,
5107                    display_name TEXT,
5108                    username TEXT,
5109                    first_name TEXT,
5110                    last_name TEXT,
5111                    avatar_url TEXT,
5112                    is_bot INTEGER,
5113                    updated_at INTEGER NOT NULL
5114                );
5115                CREATE TABLE messages (
5116                    chat_id INTEGER NOT NULL,
5117                    message_id INTEGER NOT NULL,
5118                    sender_id INTEGER NOT NULL,
5119                    timestamp INTEGER NOT NULL,
5120                    is_outgoing INTEGER NOT NULL,
5121                    content_json TEXT NOT NULL,
5122                    reply_to_message_id INTEGER,
5123                    transaction_json TEXT,
5124                    PRIMARY KEY (chat_id, message_id)
5125                );
5126                CREATE TABLE transactions (
5127                    transaction_id TEXT PRIMARY KEY,
5128                    identity_json TEXT NOT NULL,
5129                    state_json TEXT NOT NULL,
5130                    chat_id INTEGER,
5131                    message_id INTEGER,
5132                    random_id INTEGER NOT NULL,
5133                    external_source TEXT,
5134                    external_id TEXT,
5135                    failure_json TEXT,
5136                    created_at INTEGER NOT NULL,
5137                    updated_at INTEGER NOT NULL
5138                );
5139                ",
5140            )
5141            .unwrap();
5142        let initial_session = session();
5143        connection
5144            .execute(
5145                "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
5146                 VALUES (1, ?1, ?2, 1)",
5147                params![
5148                    serde_json::to_string(&initial_session.auth).unwrap(),
5149                    initial_session.account_namespace
5150                ],
5151            )
5152            .unwrap();
5153        connection
5154            .execute(
5155                "INSERT INTO dialogs (
5156                   chat_id, peer_user_id, title, last_message_id, unread_count, updated_at
5157                 ) VALUES (7, 2, 'General', 10, 1, 1)",
5158                [],
5159            )
5160            .unwrap();
5161        connection
5162            .execute(
5163                "INSERT INTO messages (
5164                   chat_id, message_id, sender_id, timestamp, is_outgoing,
5165                   content_json, reply_to_message_id, transaction_json
5166                 ) VALUES (7, 10, 2, 10, 0, ?1, NULL, NULL)",
5167                params![
5168                    serde_json::to_string(&MessageContent::Text {
5169                        text: "from the initial client".to_owned(),
5170                    })
5171                    .unwrap()
5172                ],
5173            )
5174            .unwrap();
5175        drop(connection);
5176
5177        let upgraded = SqliteStore::open(&path).unwrap();
5178        assert_eq!(
5179            upgraded
5180                .load_session()
5181                .await
5182                .unwrap()
5183                .unwrap()
5184                .account_namespace
5185                .as_deref(),
5186            Some("team")
5187        );
5188        let dialogs = upgraded.dialogs(DialogsRequest::default()).await.unwrap();
5189        assert_eq!(dialogs.dialogs.len(), 1);
5190        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
5191        assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
5192        assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("General"));
5193        let history = upgraded
5194            .history(HistoryRequest {
5195                chat_id: InlineId::new(7),
5196                limit: Some(10),
5197                before_message_id: None,
5198                after_message_id: None,
5199            })
5200            .await
5201            .unwrap();
5202        assert_eq!(history.messages.len(), 1);
5203        assert!(matches!(
5204            &history.messages[0].content,
5205            MessageContent::Text { text } if text == "from the initial client"
5206        ));
5207        assert_eq!(upgraded.sync_state().await.unwrap(), SyncState::default());
5208        upgraded
5209            .save_sync_bucket_state(SyncBucketKey::User, SyncBucketState { seq: 3, date: 30 })
5210            .await
5211            .unwrap();
5212        assert_eq!(
5213            upgraded
5214                .sync_bucket_state(SyncBucketKey::User)
5215                .await
5216                .unwrap(),
5217            SyncBucketState { seq: 3, date: 30 }
5218        );
5219
5220        drop(upgraded);
5221        let _ = std::fs::remove_file(path);
5222    }
5223
5224    #[tokio::test]
5225    async fn sqlite_store_persists_message_tombstones_reactions_and_read_state() {
5226        let path = sqlite_temp_path("message-state");
5227        let store = SqliteStore::open(&path).unwrap();
5228        let message = test_message(10);
5229        let reaction = StoredReaction {
5230            chat_id: message.chat_id,
5231            message_id: message.message_id,
5232            user_id: InlineId::new(2),
5233            reaction: "🔥".to_owned(),
5234        };
5235        let read_state = StoredReadState {
5236            chat_id: message.chat_id,
5237            read_max_id: Some(message.message_id),
5238            unread_count: Some(1),
5239            marked_unread: true,
5240        };
5241
5242        store.record_message(message.clone()).await.unwrap();
5243        store.record_reaction(reaction).await.unwrap();
5244        store.record_read_state(read_state).await.unwrap();
5245        store
5246            .record_message_deleted(message.chat_id, message.message_id)
5247            .await
5248            .unwrap();
5249        drop(store);
5250
5251        let reopened = SqliteStore::open(&path).unwrap();
5252        assert!(
5253            reopened
5254                .message_deleted(message.chat_id, message.message_id)
5255                .await
5256                .unwrap()
5257        );
5258        assert!(
5259            reopened
5260                .reactions(message.chat_id, message.message_id)
5261                .await
5262                .unwrap()
5263                .is_empty()
5264        );
5265        assert_eq!(
5266            reopened.read_state(message.chat_id).await.unwrap(),
5267            Some(read_state)
5268        );
5269
5270        reopened.record_message(message.clone()).await.unwrap();
5271        assert!(
5272            !reopened
5273                .message_deleted(message.chat_id, message.message_id)
5274                .await
5275                .unwrap()
5276        );
5277
5278        drop(reopened);
5279        let _ = std::fs::remove_file(path);
5280    }
5281
5282    #[tokio::test]
5283    async fn sqlite_store_preserves_complete_reconciliation_markers_and_chat_tombstones() {
5284        let path = sqlite_temp_path("reconciliation-state");
5285        let store = SqliteStore::open(&path).unwrap();
5286        let message = test_message(10);
5287        store
5288            .record_dialog(DialogRecord::new(message.chat_id))
5289            .await
5290            .unwrap();
5291        store.record_message(message.clone()).await.unwrap();
5292        store
5293            .record_chat_participants(message.chat_id, Vec::new())
5294            .await
5295            .unwrap();
5296        store
5297            .replace_message_reactions(
5298                message.chat_id,
5299                message.message_id,
5300                vec![StoredReaction {
5301                    chat_id: message.chat_id,
5302                    message_id: message.message_id,
5303                    user_id: InlineId::new(2),
5304                    reaction: "👍".to_owned(),
5305                }],
5306            )
5307            .await
5308            .unwrap();
5309
5310        assert!(
5311            store
5312                .chat_participants_complete(message.chat_id)
5313                .await
5314                .unwrap()
5315        );
5316        assert_eq!(
5317            store
5318                .reaction_snapshot_message_ids(message.chat_id)
5319                .await
5320                .unwrap(),
5321            vec![message.message_id]
5322        );
5323
5324        store.remove_dialog(message.chat_id).await.unwrap();
5325        drop(store);
5326        let reopened = SqliteStore::open(&path).unwrap();
5327        assert_eq!(
5328            reopened.deleted_chat_ids().await.unwrap(),
5329            vec![message.chat_id]
5330        );
5331        assert_eq!(
5332            reopened.deleted_message_ids(message.chat_id).await.unwrap(),
5333            vec![message.message_id]
5334        );
5335        assert!(
5336            !reopened
5337                .chat_participants_complete(message.chat_id)
5338                .await
5339                .unwrap()
5340        );
5341
5342        drop(reopened);
5343        let _ = std::fs::remove_file(path);
5344    }
5345
5346    #[tokio::test]
5347    async fn sqlite_store_commits_sync_cursor_and_journal_removal_atomically() {
5348        let path = sqlite_temp_path("sync-journal");
5349        let key = SyncBucketKey::Chat {
5350            peer: SyncBucketPeer::Chat {
5351                chat_id: InlineId::new(7),
5352            },
5353        };
5354        let state = SyncBucketState { seq: 9, date: 90 };
5355        let store = SqliteStore::open(&path).unwrap();
5356        store
5357            .save_pending_sync_batch(PendingSyncBatch {
5358                key,
5359                committed_state: state,
5360                payload: vec![1, 2, 3],
5361            })
5362            .await
5363            .unwrap();
5364        drop(store);
5365
5366        let reopened = SqliteStore::open(&path).unwrap();
5367        assert_eq!(
5368            reopened.pending_sync_batches().await.unwrap(),
5369            vec![PendingSyncBatch {
5370                key,
5371                committed_state: state,
5372                payload: vec![1, 2, 3],
5373            }]
5374        );
5375        let lossless = ClientEvent::MessageDeleted {
5376            chat_id: InlineId::new(7),
5377            message_id: InlineId::new(11),
5378        };
5379        let best_effort = ClientEvent::Typing {
5380            chat_id: InlineId::new(7),
5381            user_id: InlineId::new(2),
5382            is_typing: true,
5383        };
5384        let deliveries = reopened
5385            .commit_pending_sync_batch_with_events(
5386                key,
5387                state,
5388                vec![lossless.clone(), best_effort.clone()],
5389            )
5390            .await
5391            .unwrap();
5392        assert_eq!(deliveries.len(), 2);
5393        assert!(deliveries[0].delivery_id.is_some());
5394        assert_eq!(deliveries[0].event, lossless);
5395        assert_eq!(deliveries[1], ClientEventDelivery::ephemeral(best_effort));
5396        assert_eq!(reopened.sync_bucket_state(key).await.unwrap(), state);
5397        assert!(reopened.pending_sync_batches().await.unwrap().is_empty());
5398
5399        drop(reopened);
5400        let reopened = SqliteStore::open(&path).unwrap();
5401        let pending = reopened.pending_client_events().await.unwrap();
5402        assert_eq!(pending.len(), 1);
5403        assert_eq!(pending[0].event, lossless);
5404        reopened
5405            .acknowledge_client_event(pending[0].delivery_id.unwrap())
5406            .await
5407            .unwrap();
5408        assert!(reopened.pending_client_events().await.unwrap().is_empty());
5409
5410        drop(reopened);
5411        let _ = std::fs::remove_file(path);
5412    }
5413
5414    #[tokio::test]
5415    async fn sqlite_store_persists_transactions() {
5416        let path = sqlite_temp_path("transactions");
5417        let store = SqliteStore::open(&path).unwrap();
5418        let transaction = stored_transaction("txn-sqlite", TransactionState::Sent)
5419            .with_chat_id(InlineId::new(7))
5420            .with_message_id(InlineId::new(11));
5421
5422        store.record_transaction(transaction.clone()).await.unwrap();
5423        drop(store);
5424
5425        let reopened = SqliteStore::open(&path).unwrap();
5426        let loaded = reopened
5427            .transaction(transaction.identity.transaction_id.clone())
5428            .await
5429            .unwrap()
5430            .unwrap();
5431
5432        assert_eq!(
5433            loaded.identity.transaction_id,
5434            transaction.identity.transaction_id
5435        );
5436        assert_eq!(
5437            loaded.identity.external_id,
5438            transaction.identity.external_id
5439        );
5440        assert_eq!(loaded.state, TransactionState::Sent);
5441        assert_eq!(loaded.chat_id, Some(InlineId::new(7)));
5442        assert_eq!(loaded.message_id, Some(InlineId::new(11)));
5443
5444        let _ = std::fs::remove_file(path);
5445    }
5446
5447    #[tokio::test]
5448    async fn sqlite_store_replaces_messages_by_chat_and_message_id() {
5449        let store = SqliteStore::open_in_memory().unwrap();
5450
5451        store
5452            .record_message(MessageRecord {
5453                chat_id: InlineId::new(7),
5454                message_id: InlineId::new(10),
5455                sender_id: InlineId::new(2),
5456                timestamp: 100,
5457                is_outgoing: false,
5458                content: MessageContent::Text {
5459                    text: "first".to_owned(),
5460                },
5461                reply_to_message_id: None,
5462                transaction: None,
5463            })
5464            .await
5465            .unwrap();
5466        store
5467            .record_message(MessageRecord {
5468                chat_id: InlineId::new(7),
5469                message_id: InlineId::new(10),
5470                sender_id: InlineId::new(2),
5471                timestamp: 101,
5472                is_outgoing: false,
5473                content: MessageContent::Text {
5474                    text: "updated".to_owned(),
5475                },
5476                reply_to_message_id: None,
5477                transaction: None,
5478            })
5479            .await
5480            .unwrap();
5481
5482        let history = store
5483            .history(HistoryRequest {
5484                chat_id: InlineId::new(7),
5485                limit: Some(10),
5486                before_message_id: None,
5487                after_message_id: None,
5488            })
5489            .await
5490            .unwrap();
5491        assert_eq!(history.messages.len(), 1);
5492        assert!(matches!(
5493            history.messages[0].content,
5494            MessageContent::Text { ref text } if text == "updated"
5495        ));
5496    }
5497
5498    #[tokio::test]
5499    async fn sqlite_store_history_windows_match_client_cursors() {
5500        let store = SqliteStore::open_in_memory().unwrap();
5501        for message_id in 1..=4 {
5502            store
5503                .record_message(test_message(message_id))
5504                .await
5505                .unwrap();
5506        }
5507
5508        let latest = store
5509            .history(HistoryRequest {
5510                chat_id: InlineId::new(7),
5511                limit: Some(2),
5512                before_message_id: None,
5513                after_message_id: None,
5514            })
5515            .await
5516            .unwrap();
5517        assert!(latest.has_more);
5518        assert_eq!(
5519            latest
5520                .messages
5521                .iter()
5522                .map(|message| message.message_id)
5523                .collect::<Vec<_>>(),
5524            vec![InlineId::new(3), InlineId::new(4)]
5525        );
5526
5527        let newer = store
5528            .history(HistoryRequest {
5529                chat_id: InlineId::new(7),
5530                limit: Some(2),
5531                before_message_id: None,
5532                after_message_id: Some(InlineId::new(1)),
5533            })
5534            .await
5535            .unwrap();
5536        assert!(newer.has_more);
5537        assert_eq!(
5538            newer
5539                .messages
5540                .iter()
5541                .map(|message| message.message_id)
5542                .collect::<Vec<_>>(),
5543            vec![InlineId::new(2), InlineId::new(3)]
5544        );
5545
5546        let older = store
5547            .history(HistoryRequest {
5548                chat_id: InlineId::new(7),
5549                limit: Some(2),
5550                before_message_id: Some(InlineId::new(4)),
5551                after_message_id: None,
5552            })
5553            .await
5554            .unwrap();
5555        assert!(older.has_more);
5556        assert_eq!(
5557            older
5558                .messages
5559                .iter()
5560                .map(|message| message.message_id)
5561                .collect::<Vec<_>>(),
5562            vec![InlineId::new(2), InlineId::new(3)]
5563        );
5564    }
5565
5566    fn sqlite_temp_path(label: &str) -> std::path::PathBuf {
5567        let unique = SystemTime::now()
5568            .duration_since(UNIX_EPOCH)
5569            .unwrap()
5570            .as_nanos();
5571        std::env::temp_dir().join(format!(
5572            "inline-client-{label}-{}-{unique}.db",
5573            std::process::id()
5574        ))
5575    }
5576
5577    fn stored_transaction(id: &str, state: TransactionState) -> StoredTransaction {
5578        StoredTransaction::new(
5579            TransactionIdentity::new(
5580                TransactionId::try_new(id).unwrap(),
5581                Some(ExternalId::try_new("host-event", "event-1").unwrap()),
5582                RandomId::new(42),
5583            ),
5584            state,
5585        )
5586    }
5587}