Files
mnote/rust/crates/mnote-web/src/routes/local_folder_events.rs
T

144 lines
4.7 KiB
Rust
Raw Normal View History

use crate::app::AppState;
2026-05-08 11:23:08 +08:00
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::local_folder_source::{
decode_local_id_segment, ensure_local_workspace_read_access,
};
use axum::extract::{Extension, Query, State};
2026-05-08 11:23:08 +08:00
use axum::http::{HeaderMap, HeaderName, HeaderValue};
use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
use futures_util::stream;
use serde::Deserialize;
use serde_json::{json, Value};
use std::convert::Infallible;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::broadcast::error::RecvError;
2026-05-08 11:23:08 +08:00
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalFolderEventsQuery {
pub root_uri: String,
pub document_id: Option<String>,
}
pub async fn local_folder_events(
State(state): State<AppState>,
2026-05-08 11:23:08 +08:00
Extension(context): Extension<RequestContext>,
Query(query): Query<LocalFolderEventsQuery>,
) -> Result<
(
HeaderMap,
Sse<impl futures_util::Stream<Item = Result<SseEvent, Infallible>>>,
),
WebError,
> {
let canonical_root = ensure_local_workspace_read_access(&context, &query.root_uri)
.map_err(|error| error.with_context(&context))?;
2026-05-08 11:23:08 +08:00
let document_relative_path = query
.document_id
.as_deref()
.and_then(local_markdown_relative_path_from_document_id);
let subscription = state
.local_folder_watcher_registry()
.subscribe(&canonical_root)
.map_err(|error| WebError::internal(error).with_context(&context))?;
2026-05-08 11:23:08 +08:00
let initial = json!({
"sourceKind": "local_folder",
"rootUri": subscription.root_uri(),
2026-05-08 11:23:08 +08:00
"documentId": query.document_id,
"revision": system_time_ms(SystemTime::now()),
});
let stream = stream::unfold(
(Some(initial), subscription, document_relative_path),
|(initial, mut subscription, document_relative_path)| async move {
2026-05-11 13:16:34 +08:00
if let Some(payload) = initial {
return Some((
Ok(stream_event("ready", &payload)),
(None, subscription, document_relative_path),
));
}
loop {
match subscription.receiver.recv().await {
Ok(payload) => {
if let Some(expected) = document_relative_path.as_deref() {
let relative_path = payload
.get("relativePath")
.and_then(Value::as_str)
.unwrap_or_default();
if expected != relative_path {
continue;
}
}
return Some((
Ok(stream_event("change", &payload)),
(None, subscription, document_relative_path),
));
}
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return None,
}
}
},
);
2026-05-08 11:23:08 +08:00
let mut headers = HeaderMap::new();
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-web-owner") {
headers.insert(name, HeaderValue::from_static("mnote-web"));
}
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-local-folder-events-owner") {
headers.insert(name, HeaderValue::from_static("rust-web"));
}
Ok((
headers,
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(Duration::from_secs(15))
.text("keepalive"),
),
))
}
fn local_markdown_relative_path_from_document_id(document_id: &str) -> Option<String> {
let trimmed = document_id.trim();
let encoded = trimmed.strip_prefix("local-md:")?;
decode_local_id_segment(encoded).ok()
}
fn system_time_ms(time: SystemTime) -> u128 {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or(0)
}
fn stream_event(event_name: &str, payload: &Value) -> SseEvent {
let id = payload
.get("revision")
.and_then(|value| {
value
.as_str()
.map(ToOwned::to_owned)
.or_else(|| value.as_u64().map(|number| number.to_string()))
})
.unwrap_or_else(|| "0".to_string());
SseEvent::default()
.event(event_name)
.id(id)
.json_data(payload)
.expect("SSE 事件必须可序列化")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_markdown_document_id_maps_to_relative_path() {
assert_eq!(
2026-05-11 13:16:34 +08:00
local_markdown_relative_path_from_document_id("local-md:docs~2FREADME.md").as_deref(),
2026-05-08 11:23:08 +08:00
Some("docs/README.md")
);
}
}