feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -10,16 +10,16 @@ use crate::routes::snapshot_support::ProjectionSnapshot;
use axum::extract::{Extension, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue};
use axum::response::sse::{Event as SseEvent, Sse};
use futures_util::stream;
use futures_util::StreamExt;
use futures_util::stream;
use serde::Deserialize;
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::convert::Infallible;
use std::path::PathBuf;
use std::pin::Pin;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::broadcast::error::RecvError;
use tokio::time::{interval, timeout, MissedTickBehavior};
use tokio::time::{MissedTickBehavior, interval, timeout};
type BoxedEventStream =
Pin<Box<dyn futures_util::Stream<Item = Result<SseEvent, Infallible>> + Send>>;
@@ -29,6 +29,7 @@ type BoxedEventStream =
pub struct LocalFolderEventsQuery {
pub root_uri: String,
pub document_id: Option<String>,
pub resource_path: Option<String>,
pub tree_live: Option<bool>,
}
@@ -68,7 +69,8 @@ async fn build_document_events_stream(
let document_relative_path = query
.document_id
.as_deref()
.and_then(local_markdown_relative_path_from_document_id);
.and_then(local_markdown_relative_path_from_document_id)
.or_else(|| query.resource_path.as_deref().and_then(normalize_resource_event_path));
let subscription = state
.local_folder_watcher_registry()
.subscribe(&canonical_root)
@@ -78,6 +80,7 @@ async fn build_document_events_stream(
"sourceKind": "local_folder",
"rootUri": subscription.root_uri(),
"documentId": query.document_id,
"resourcePath": query.resource_path,
"revision": system_time_ms(SystemTime::now()),
});
let stream = stream::unfold(
@@ -117,7 +120,7 @@ async fn build_document_events_stream(
/// with full sidebar + file tree projections.
///
/// Reuses `LocalFolderWatcherRegistry` — no second watcher created.
/// No data is written to Convex command log.
/// No data is written to an external/cloud command log.
async fn build_tree_live_stream(
state: AppState,
context: RequestContext,
@@ -423,6 +426,18 @@ fn local_markdown_relative_path_from_document_id(document_id: &str) -> Option<St
decode_local_id_segment(encoded).ok()
}
fn normalize_resource_event_path(resource_path: &str) -> Option<String> {
let trimmed = resource_path.trim().trim_start_matches('/');
if trimmed.is_empty()
|| PathBuf::from(trimmed)
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return None;
}
Some(trimmed.replace('\\', "/"))
}
fn document_event_targets_relative_path(payload: &Value, expected: &str) -> bool {
let relative_path = payload
.get("relativePath")
@@ -457,7 +472,7 @@ fn stream_event(event_name: &str, payload: &Value) -> SseEvent {
#[cfg(test)]
mod tests {
use super::*;
use crate::app::{build_app, AppConfig, AppState};
use crate::app::{AppConfig, AppState, build_app};
use crate::routes::local_folder_source::initialize_local_workspace_for_actor;
use axum::body::Body;
use axum::http::Request;
@@ -577,6 +592,35 @@ mod tests {
);
}
#[test]
fn resource_event_path_filters_to_its_own_relative_path() {
let expected = normalize_resource_event_path("README.assets/slides.pptx")
.expect("resource path should normalize");
let payload = json!({
"sourceKind": "local_folder",
"rootUri": "file:///test",
"relativePath": "README.assets/slides.pptx",
"documentId": "",
"eventKind": "Modify(Data)",
"revision": 1,
});
let other_payload = json!({
"sourceKind": "local_folder",
"rootUri": "file:///test",
"relativePath": "README.assets/report.pdf",
"documentId": "",
"eventKind": "Modify(Data)",
"revision": 2,
});
assert!(document_event_targets_relative_path(&payload, &expected));
assert!(!document_event_targets_relative_path(&other_payload, &expected));
assert!(
normalize_resource_event_path("../escape.pdf").is_none(),
"resource watch path 不能越过 root"
);
}
#[test]
fn build_tree_snapshot_payload_has_required_fields() {
let sidebar_projection = json!({
@@ -697,11 +741,13 @@ mod tests {
&& parent["reason"].as_str() == Some("child-watch")),
"watch batch 应声明 docs affected parent: {payload}"
);
assert!(payload["eventKinds"]
.as_array()
.expect("event kinds")
.iter()
.any(|kind| kind.as_str() == Some("Modify(Data)")));
assert!(
payload["eventKinds"]
.as_array()
.expect("event kinds")
.iter()
.any(|kind| kind.as_str() == Some("Modify(Data)"))
);
let _ = std::fs::remove_dir_all(root);
}