blob: 4d5202061f21d9b9b3da456a8fc5fa050d8fefb1 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sqlx::{Connection, SqliteConnection};
use super::{MAX_SAFE_INTEGER, SessionRecord, advance_catalog, invalid, validate_id};
use crate::{EventLog, StoreError};
/// Mutable control authority, independent of disposable execution projections.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionReadState {
pub has_unread: bool,
pub last_read_message_id: Option<String>,
}
pub(super) async fn read(
connection: &mut SqliteConnection,
id: &str,
) -> Result<SessionReadState, StoreError> {
let state: Option<(bool, Option<String>)> = sqlx::query_as(
"SELECT has_unread, last_read_message_id FROM session_read_state WHERE session_id = ?",
)
.bind(id)
.fetch_optional(connection)
.await?;
Ok(state.map_or_else(
SessionReadState::default,
|(has_unread, last_read_message_id)| SessionReadState {
has_unread,
last_read_message_id,
},
))
}
/// Called only for newly inserted terminal facts, in their append transaction.
pub(crate) async fn mark_unread(
connection: &mut SqliteConnection,
id: &str,
invocation: &str,
) -> Result<(), StoreError> {
sqlx::query(
"INSERT INTO session_read_state (session_id, has_unread, last_read_message_id)
SELECT id, 1, NULL FROM session_control WHERE id = ?1
AND NOT EXISTS (SELECT 1 FROM runtime_events
WHERE invocation_id = ?2 AND kind = 'invocation_opened'
AND json_extract(event_json, '$.fact.input.kind') = 'context_compact')
ON CONFLICT(session_id) DO UPDATE SET has_unread = 1",
)
.bind(id)
.bind(invocation)
.execute(connection)
.await?;
Ok(())
}
impl EventLog {
/// Acknowledge only the durable visible tail, including during active execution.
pub async fn set_session_read_marker<T: DeserializeOwned + Send + 'static>(
&self,
id: &str,
message_id: &str,
) -> Result<SessionRecord<T>, StoreError> {
self.validate_root()?;
validate_id(id)?;
validate_id(message_id)?;
let (id, message_id) = (id.to_owned(), message_id.to_owned());
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
let record: SessionRecord<T> = super::read(&mut tx, &id)
.await?.ok_or(StoreError::SessionNotFound)?;
let latest = super::activity::latest_visible_message(&mut tx, &id).await?;
if latest.as_deref() != Some(message_id.as_str())
|| (!record.read_state.has_unread
&& record.read_state.last_read_message_id.as_deref() == Some(message_id.as_str()))
{
return Ok(record);
}
sqlx::query(
"INSERT INTO session_read_state (session_id, has_unread, last_read_message_id)
VALUES (?, 0, ?) ON CONFLICT(session_id) DO UPDATE
SET has_unread = 0, last_read_message_id = excluded.last_read_message_id",
).bind(&id).bind(&message_id).execute(&mut *tx).await?;
if sqlx::query(
"UPDATE session_control SET revision = revision + 1 WHERE id = ? AND revision < ?",
).bind(&id).bind(MAX_SAFE_INTEGER as i64).execute(&mut *tx).await?.rows_affected() != 1 {
return Err(invalid("session revision exhausted"));
}
advance_catalog(&mut tx).await?;
let record = super::read(&mut tx, &id).await?.ok_or(StoreError::SessionNotFound)?;
tx.commit().await.map_err(StoreError::CommitUnknown)?;
Ok(record)
})
})
.await
}
}