JMAP/changes: Return the correct container/item change id when there are no changes

This commit is contained in:
mdecimus
2026-01-19 16:00:51 -03:00
parent 3c0312df1e
commit a05a047bd3
3 changed files with 60 additions and 27 deletions

View File

@@ -4,8 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use crate::api::auth::JmapAuthorization; use crate::{api::auth::JmapAuthorization, changes::state::JmapCacheState};
use common::{Server, auth::AccessToken}; use common::{Server, auth::AccessToken};
use email::cache::MessageCacheFetch;
use groupware::cache::GroupwareCache;
use jmap_proto::{ use jmap_proto::{
method::changes::{ChangesRequest, ChangesResponse}, method::changes::{ChangesRequest, ChangesResponse},
object::{JmapObject, NullObject, mailbox::MailboxProperty}, object::{JmapObject, NullObject, mailbox::MailboxProperty},
@@ -15,6 +17,7 @@ use jmap_proto::{
}; };
use std::future::Future; use std::future::Future;
use store::query::log::{Change, Query}; use store::query::log::{Change, Query};
use trc::AddContext;
use types::collection::{Collection, SyncCollection}; use types::collection::{Collection, SyncCollection};
pub trait ChangesLookup: Sync + Send { pub trait ChangesLookup: Sync + Send {
@@ -139,12 +142,41 @@ impl ChangesLookup for Server {
(0, changelog) (0, changelog)
} }
State::Exact(change_id) => ( State::Exact(change_id) => {
0, let last_state = match collection {
self.store() SyncCollection::Calendar | SyncCollection::AddressBook => self
.changes(account_id, collection.into(), Query::Since(*change_id)) .fetch_dav_resources(access_token, account_id, collection)
.await?, .await
), .caused_by(trc::location!())?
.get_state(is_container)
.into(),
SyncCollection::Email => self
.get_cached_messages(account_id)
.await?
.get_state(is_container)
.into(),
_ => None,
};
if let Some(last_state) = last_state {
response.new_state = last_state;
if response.new_state == State::Exact(*change_id) {
return Ok(IntermediateChangesResponse {
response,
object,
only_container_changes: false,
});
}
}
(
0,
self.store()
.changes(account_id, collection.into(), Query::Since(*change_id))
.await?,
)
}
State::Intermediate(intermediate_state) => { State::Intermediate(intermediate_state) => {
let changelog = self let changelog = self
.store() .store()
@@ -175,10 +207,16 @@ impl ChangesLookup for Server {
} }
}; };
if changelog.is_truncated && request.since_state != State::Initial { if (changelog.is_truncated || changelog.from_change_id == 0)
return Err(trc::JmapEvent::CannotCalculateChanges && request.since_state != State::Initial
.into_err() {
.details("Changelog has been truncated")); return Err(trc::JmapEvent::CannotCalculateChanges.into_err().details(
if changelog.is_truncated {
"Change log is truncated"
} else {
"Since state is invalid"
},
));
} }
let mut changes = changelog let mut changes = changelog
@@ -218,15 +256,15 @@ impl ChangesLookup for Server {
.unwrap_or(changelog.to_change_id); .unwrap_or(changelog.to_change_id);
response.has_more_changes = changes.peek().is_some(); response.has_more_changes = changes.peek().is_some();
response.new_state = if response.has_more_changes { if response.has_more_changes {
State::new_intermediate( response.new_state = State::new_intermediate(
changelog.from_change_id, changelog.from_change_id,
change_id, change_id,
items_sent + max_changes, items_sent + max_changes,
) );
} else { } else if response.new_state == State::Initial {
State::new_exact(change_id) response.new_state = State::new_exact(change_id)
}; }
Ok(IntermediateChangesResponse { Ok(IntermediateChangesResponse {
only_container_changes: is_container && !response.updated.is_empty() && !items_changed, only_container_changes: is_container && !response.updated.is_empty() && !items_changed,

View File

@@ -120,6 +120,9 @@ impl Store {
} else { } else {
changelog.changes.push(Change::InsertItem(change_id)); changelog.changes.push(Change::InsertItem(change_id));
} }
} else {
changelog.from_change_id = change_id;
changelog.to_change_id = change_id;
} }
Ok(true) Ok(true)
}, },
@@ -127,15 +130,6 @@ impl Store {
.await .await
.caused_by(trc::location!())?; .caused_by(trc::location!())?;
// A non-existing change id was requested, return the last change id
if changelog.changes.is_empty() && from_change_id != 0 && changelog.from_change_id == 0 {
changelog.from_change_id = self
.get_last_change_id(account_id, collection_)
.await?
.unwrap_or_default();
changelog.to_change_id = changelog.from_change_id;
}
Ok(changelog) Ok(changelog)
} }

View File

@@ -198,7 +198,7 @@ pub async fn test(params: &mut JMAPTest) {
state state
); );
if let State::Initial = state { if &State::Initial == state {
new_state = State::parse_str(changes.new_state()).unwrap(); new_state = State::parse_str(changes.new_state()).unwrap();
} }
@@ -312,6 +312,7 @@ pub async fn test(params: &mut JMAPTest) {
assert_eq!(created, vec![2, 3, 11, 12]); assert_eq!(created, vec![2, 3, 11, 12]);
assert_eq!(changes.updated(), Vec::<String>::new()); assert_eq!(changes.updated(), Vec::<String>::new());
assert_eq!(changes.destroyed(), Vec::<String>::new()); assert_eq!(changes.destroyed(), Vec::<String>::new());
params.destroy_all_mailboxes(account).await;
params.assert_is_empty().await; params.assert_is_empty().await;
} }