Skip to main content

matrix_sdk_indexeddb/event_cache_store/
migrations.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License
14
15use indexed_db_futures::{
16    database::Database,
17    error::{DomException, Error, OpenDbError},
18    transaction::Transaction,
19};
20use thiserror::Error;
21
22/// The current version and keys used in the database.
23pub mod current {
24    use super::{Version, v4};
25
26    pub const VERSION: Version = Version::V4;
27    pub use v4::keys;
28}
29
30/// Opens a connection to the IndexedDB database and takes care of upgrading it
31/// if necessary.
32#[allow(unused)]
33pub async fn open_and_upgrade_db(name: &str) -> Result<Database, OpenDbError> {
34    Database::open(name)
35        .with_version(current::VERSION as u32)
36        .with_on_upgrade_needed(|event, transaction| {
37            let mut version = Version::try_from(event.old_version() as u32)?;
38            while version < current::VERSION {
39                version = match version.upgrade(transaction)? {
40                    Some(next) => next,
41                    None => current::VERSION, /* No more upgrades to apply, jump forward! */
42                };
43            }
44            Ok(())
45        })
46        .await
47}
48
49/// Represents the version of the IndexedDB database.
50#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
51#[repr(u32)]
52pub enum Version {
53    /// Version 0 of the database, for details see [`v0`].
54    V0 = 0,
55    /// Version 1 of the database, for details see [`v1`].
56    V1 = 1,
57    /// Version 2 of the database, for details see [`v2`].
58    V2 = 2,
59    /// Version 3 of the database, for details see [`v3`].
60    V3 = 3,
61    /// Version 4 of the database, for details see [`v4`].
62    V4 = 4,
63}
64
65impl Version {
66    /// Upgrade the database to the next version, if one exists.
67    pub fn upgrade(self, transaction: &Transaction<'_>) -> Result<Option<Self>, Error> {
68        match self {
69            Self::V0 => v0::upgrade(transaction).map(Some),
70            Self::V1 => v1::upgrade(transaction).map(Some),
71            Self::V2 => v2::upgrade(transaction).map(Some),
72            Self::V3 => v3::upgrade(transaction).map(Some),
73            Self::V4 => Ok(None),
74        }
75    }
76}
77
78#[derive(Debug, Error)]
79#[error("unknown version: {0}")]
80pub struct UnknownVersionError(u32);
81
82impl TryFrom<u32> for Version {
83    type Error = UnknownVersionError;
84
85    fn try_from(value: u32) -> Result<Self, Self::Error> {
86        match value {
87            0 => Ok(Version::V0),
88            1 => Ok(Version::V1),
89            2 => Ok(Version::V2),
90            3 => Ok(Version::V3),
91            4 => Ok(Version::V4),
92            v => Err(UnknownVersionError(v)),
93        }
94    }
95}
96
97impl From<UnknownVersionError> for Error {
98    fn from(value: UnknownVersionError) -> Self {
99        let message = format!("unknown version: {}", value.0);
100        let name = "UnknownVersionError";
101        match web_sys::DomException::new_with_message_and_name(&message, name) {
102            Ok(inner) => Self::DomException(DomException::DataError(inner)),
103            Err(err) => err.into(),
104        }
105    }
106}
107
108pub mod v0 {
109    use super::*;
110
111    /// Upgrade database from `v0` to `v1`
112    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
113        v1::create_object_stores(transaction.db())?;
114        Ok(Version::V1)
115    }
116}
117
118pub mod v1 {
119    use indexed_db_futures::Build;
120
121    use super::*;
122
123    pub mod keys {
124        pub const LEASES: &str = "leases";
125        pub const LEASES_KEY_PATH: &str = "id";
126        pub const ROOMS: &str = "rooms";
127        pub const LINKED_CHUNK_IDS: &str = "linked_chunk_ids";
128        pub const LINKED_CHUNKS: &str = "linked_chunks";
129        pub const LINKED_CHUNKS_KEY_PATH: &str = "id";
130        pub const LINKED_CHUNKS_NEXT: &str = "linked_chunks_next";
131        pub const LINKED_CHUNKS_NEXT_KEY_PATH: &str = "next";
132        pub const EVENTS: &str = "events";
133        pub const EVENTS_KEY_PATH: &str = "id";
134        pub const EVENTS_ROOM: &str = "events_room";
135        pub const EVENTS_ROOM_KEY_PATH: &str = "room";
136        pub const EVENTS_POSITION: &str = "events_position";
137        pub const EVENTS_POSITION_KEY_PATH: &str = "position";
138        pub const EVENTS_RELATION: &str = "events_relation";
139        pub const EVENTS_RELATION_KEY_PATH: &str = "relation";
140        pub const EVENTS_RELATION_RELATED_EVENTS: &str = "events_relation_related_event";
141        pub const EVENTS_RELATION_RELATION_TYPES: &str = "events_relation_relation_type";
142        pub const GAPS: &str = "gaps";
143        pub const GAPS_KEY_PATH: &str = "id";
144    }
145
146    /// Create all object stores and indices for v1 database
147    pub fn create_object_stores(db: &Database) -> Result<(), Error> {
148        create_lease_object_store(db)?;
149        create_linked_chunks_object_store(db)?;
150        create_events_object_store(db)?;
151        create_gaps_object_store(db)?;
152        Ok(())
153    }
154
155    /// Create an object store tracking leases on time-based locks
156    fn create_lease_object_store(db: &Database) -> Result<(), Error> {
157        let _ = db
158            .create_object_store(keys::LEASES)
159            .with_key_path(keys::LEASES_KEY_PATH.into())
160            .build()?;
161        Ok(())
162    }
163
164    /// Create an object store for tracking information about linked chunks.
165    ///
166    /// * Primary Key - `id`
167    /// * Index - `is_last` - tracks the last chunk in linked chunks
168    fn create_linked_chunks_object_store(db: &Database) -> Result<(), Error> {
169        let _ = db
170            .create_object_store(keys::LINKED_CHUNKS)
171            .with_key_path(keys::LINKED_CHUNKS_KEY_PATH.into())
172            .build()?
173            .create_index(keys::LINKED_CHUNKS_NEXT, keys::LINKED_CHUNKS_NEXT_KEY_PATH.into())
174            .build()?;
175        Ok(())
176    }
177
178    /// Create an object store for tracking information about events.
179    ///
180    /// * Primary Key - `id`
181    /// * Index (unique) - `room` - tracks whether an event is in a given room
182    /// * Index (unique) - `position` - tracks position of an event in linked
183    ///   chunks
184    /// * Index - `relation` - tracks any event to which the given event is
185    ///   related
186    fn create_events_object_store(db: &Database) -> Result<(), Error> {
187        let events = db
188            .create_object_store(keys::EVENTS)
189            .with_key_path(keys::EVENTS_KEY_PATH.into())
190            .build()?;
191        let _ = events
192            .create_index(keys::EVENTS_ROOM, keys::EVENTS_ROOM_KEY_PATH.into())
193            .with_unique(true)
194            .build()?;
195        let _ = events
196            .create_index(keys::EVENTS_POSITION, keys::EVENTS_POSITION_KEY_PATH.into())
197            .with_unique(true)
198            .build()?;
199        let _ = events
200            .create_index(keys::EVENTS_RELATION, keys::EVENTS_RELATION_KEY_PATH.into())
201            .build()?;
202        Ok(())
203    }
204
205    /// Create an object store for tracking information about gaps.
206    ///
207    /// * Primary Key - `id`
208    fn create_gaps_object_store(db: &Database) -> Result<(), Error> {
209        let _ =
210            db.create_object_store(keys::GAPS).with_key_path(keys::GAPS_KEY_PATH.into()).build()?;
211        Ok(())
212    }
213
214    /// Upgrade database from `v1` to `v2`
215    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
216        v2::empty_leases(transaction)?;
217        Ok(Version::V2)
218    }
219}
220
221mod v2 {
222    // Re-use all the same keys from `v1`.
223    pub use super::v1::keys;
224    use super::*;
225
226    /// The format of [`Lease`][super::super::types::Lease] is changing. Let's
227    /// erase previous values.
228    pub fn empty_leases(transaction: &Transaction<'_>) -> Result<(), Error> {
229        let object_store = transaction.object_store(keys::LEASES)?;
230
231        // Remove all previous leases.
232        object_store.clear()?;
233
234        Ok(())
235    }
236
237    /// Upgrade database from `v2` to `v3`
238    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
239        v3::update_events_object_store(transaction)?;
240        Ok(Version::V3)
241    }
242}
243
244mod v3 {
245    use indexed_db_futures::Build;
246
247    // Re-use all the same keys from `v2`.
248    pub use super::v2::keys;
249    use super::*;
250
251    /// Update the events object store, so that the `room` index is no longer
252    /// unique. This allows an event to be stored in a room twice - e.g., once
253    /// in the main thread and once in a side thread.
254    ///
255    /// Note that this operation removes the existing events object store and
256    /// all of its contents.
257    ///
258    /// **Bug**: This migration failed to also clear the `linked_chunks` and
259    /// `gaps` stores, leaving orphaned chunk structures that reference events
260    /// which no longer exist. This is corrected by the V4 migration.
261    pub fn update_events_object_store(transaction: &Transaction<'_>) -> Result<(), Error> {
262        remove_events_object_store(transaction)?;
263        create_events_object_store(transaction.db())?;
264        Ok(())
265    }
266
267    /// Remove events object store
268    pub fn remove_events_object_store(transaction: &Transaction<'_>) -> Result<(), Error> {
269        let object_store = transaction.object_store(keys::EVENTS)?;
270        // It is faster to clear all events first, then delete the object store rather
271        // than immediately deleting.
272        //
273        // For details, see https://www.artificialworlds.net/blog/2024/02/02/deleting-an-indexed-db-store-can-be-incredibly-slow-on-firefox/
274        object_store.clear()?;
275        transaction.db().delete_object_store(keys::EVENTS)?;
276        Ok(())
277    }
278
279    /// Create an object store for tracking information about events.
280    ///
281    /// * Primary Key - `id`
282    /// * Index - `room` - tracks whether an event is in a given room
283    /// * Index (unique) - `position` - tracks position of an event in linked
284    ///   chunks
285    /// * Index - `relation` - tracks any event to which the given event is
286    ///   related
287    pub fn create_events_object_store(db: &Database) -> Result<(), Error> {
288        let events = db
289            .create_object_store(keys::EVENTS)
290            .with_key_path(keys::EVENTS_KEY_PATH.into())
291            .build()?;
292        let _ =
293            events.create_index(keys::EVENTS_ROOM, keys::EVENTS_ROOM_KEY_PATH.into()).build()?;
294        let _ = events
295            .create_index(keys::EVENTS_POSITION, keys::EVENTS_POSITION_KEY_PATH.into())
296            .with_unique(true)
297            .build()?;
298        let _ = events
299            .create_index(keys::EVENTS_RELATION, keys::EVENTS_RELATION_KEY_PATH.into())
300            .build()?;
301        Ok(())
302    }
303
304    /// Upgrade database from `v3` to `v4`
305    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
306        v4::empty_event_cache(transaction)?;
307        Ok(Version::V4)
308    }
309}
310
311mod v4 {
312    // Re-use all the same keys from `v3`.
313    pub use super::v3::keys;
314    use super::*;
315
316    /// The V3 migration only cleared the events object store but left
317    /// linked_chunks and gaps intact. This created an inconsistent state where
318    /// chunk structures reference events that no longer exist, and no gap
319    /// chunks exist to trigger backfill from the server.
320    ///
321    /// This migration clears linked_chunks and gaps for users who were already
322    /// migrated to V3, so rooms start fresh and properly re-sync.
323    pub fn empty_event_cache(transaction: &Transaction<'_>) -> Result<(), Error> {
324        let linked_chunks = transaction.object_store(keys::LINKED_CHUNKS)?;
325        linked_chunks.clear()?;
326
327        let gaps = transaction.object_store(keys::GAPS)?;
328        gaps.clear()?;
329
330        let events = transaction.object_store(keys::EVENTS)?;
331        events.clear()?;
332
333        Ok(())
334    }
335}